404
5 |{{ getMsg() }}6 |
├── src
├── .vuepress
│ ├── components
│ │ └── .gitkeep
│ ├── override.styl
│ ├── public
│ │ ├── face.jpg
│ │ ├── favicon.ico
│ │ ├── icons
│ │ │ ├── 192.png
│ │ │ ├── 512.png
│ │ │ └── favicon.png
│ │ └── manifest.json
│ ├── theme
│ │ ├── imgs
│ │ │ └── brand.jpg
│ │ ├── styles
│ │ │ ├── fonts
│ │ │ │ └── roboto
│ │ │ │ │ ├── Roboto-Bold.eot
│ │ │ │ │ ├── Roboto-Bold.ttf
│ │ │ │ │ ├── Roboto-Bold.woff
│ │ │ │ │ ├── Roboto-Light.eot
│ │ │ │ │ ├── Roboto-Light.ttf
│ │ │ │ │ ├── Roboto-Thin.eot
│ │ │ │ │ ├── Roboto-Thin.ttf
│ │ │ │ │ ├── Roboto-Thin.woff
│ │ │ │ │ ├── Roboto-Bold.woff2
│ │ │ │ │ ├── Roboto-Light.woff
│ │ │ │ │ ├── Roboto-Light.woff2
│ │ │ │ │ ├── Roboto-Medium.eot
│ │ │ │ │ ├── Roboto-Medium.ttf
│ │ │ │ │ ├── Roboto-Medium.woff
│ │ │ │ │ ├── Roboto-Medium.woff2
│ │ │ │ │ ├── Roboto-Regular.eot
│ │ │ │ │ ├── Roboto-Regular.ttf
│ │ │ │ │ ├── Roboto-Regular.woff
│ │ │ │ │ ├── Roboto-Thin.woff2
│ │ │ │ │ └── Roboto-Regular.woff2
│ │ │ ├── theme.styl
│ │ │ ├── global.styl
│ │ │ ├── config.styl
│ │ │ ├── roboto.styl
│ │ │ └── content.styl
│ │ ├── About.vue
│ │ ├── NotFound.vue
│ │ ├── libs
│ │ │ ├── i18n.js
│ │ │ ├── utils.js
│ │ │ ├── routes.js
│ │ │ ├── socials.js
│ │ │ └── blog.js
│ │ ├── components
│ │ │ ├── Comment.vue
│ │ │ ├── PostTime.vue
│ │ │ ├── Tag.vue
│ │ │ ├── Pagination.vue
│ │ │ ├── index.js
│ │ │ ├── Share.vue
│ │ │ └── PostCard.vue
│ │ ├── languages
│ │ │ ├── zh-CN.js
│ │ │ └── en-US.js
│ │ ├── enhanceApp.js
│ │ ├── Home.vue
│ │ ├── Header.vue
│ │ ├── Tags.vue
│ │ ├── Post.vue
│ │ ├── Footer.vue
│ │ ├── SideNav.vue
│ │ └── Layout.vue
│ ├── ignoreStylus.js
│ └── config.js
├── index.md
├── about
│ └── index.md
├── tags
│ └── index.md
└── posts
│ ├── text-truncation.md
│ ├── test-markdown.md
│ ├── webpack-use-lodash.md
│ ├── write-good-front-end-component.md
│ └── cursor-offset-at-input.md
├── lib
├── noop.js
├── index.js
├── default-theme
│ ├── search.svg
│ ├── styles
│ │ ├── config.styl
│ │ ├── arrow.styl
│ │ ├── custom-blocks.styl
│ │ ├── mobile.styl
│ │ ├── nprogress.styl
│ │ ├── code.styl
│ │ └── theme.styl
│ ├── NotFound.vue
│ ├── OutboundLink.vue
│ ├── DropdownTransition.vue
│ ├── NavLink.vue
│ ├── SidebarButton.vue
│ ├── SidebarGroup.vue
│ ├── Navbar.vue
│ ├── Sidebar.vue
│ ├── SidebarLink.vue
│ ├── Home.vue
│ ├── Page.vue
│ ├── AlgoliaSearchBox.vue
│ ├── NavLinks.vue
│ ├── DropdownLink.vue
│ ├── Layout.vue
│ ├── SearchBox.vue
│ └── util.js
├── app
│ ├── index.dev.html
│ ├── ClientOnly.js
│ ├── Content.js
│ ├── serverEntry.js
│ ├── util.js
│ ├── index.ssr.html
│ ├── clientEntry.js
│ ├── dataMixin.js
│ └── app.js
├── markdown
│ ├── hoist.js
│ ├── slugify.js
│ ├── containers.js
│ ├── highlight.js
│ ├── index.js
│ ├── highlightLines.js
│ ├── link.js
│ └── component.js
├── webpack
│ ├── HeadPlugin.js
│ ├── createServerConfig.js
│ ├── createClientConfig.js
│ ├── ClientPlugin.js
│ ├── markdownLoader.js
│ └── createBaseConfig.js
├── eject.js
├── util
│ └── index.js
├── dev.js
└── build.js
├── .gitignore
├── docs
├── face.jpg
├── favicon.ico
├── icons
│ ├── 192.png
│ ├── 512.png
│ └── favicon.png
├── assets
│ ├── img
│ │ ├── brand.734f817b.jpg
│ │ └── face.55c9d89d.jpg
│ ├── fonts
│ │ ├── Roboto-Bold.39b2c303.woff2
│ │ ├── Roboto-Bold.dc81817d.woff
│ │ ├── Roboto-Bold.e31fcf18.ttf
│ │ ├── Roboto-Light.3b813c2a.woff
│ │ ├── Roboto-Light.46e48ce0.ttf
│ │ ├── Roboto-Medium.894a2ede.ttf
│ │ ├── Roboto-Thin.7500519d.woff
│ │ ├── Roboto-Thin.94998475.ttf
│ │ ├── Roboto-Thin.954bbdeb.woff2
│ │ ├── fa-brands-400.c601db56.ttf
│ │ ├── fa-brands-400.e2a7835b.eot
│ │ ├── fa-solid-900.24f9359f.eot
│ │ ├── fa-solid-900.4ff89f93.woff
│ │ ├── fa-solid-900.af4698a4.ttf
│ │ ├── Roboto-Light.69f8a061.woff2
│ │ ├── Roboto-Medium.574fd0b5.woff2
│ │ ├── Roboto-Medium.fc78759e.woff
│ │ ├── Roboto-Regular.ba3dcd89.woff
│ │ ├── Roboto-Regular.df7b648c.ttf
│ │ ├── fa-brands-400.9404b3cb.woff2
│ │ ├── fa-brands-400.cc6aff50.woff
│ │ ├── fa-regular-400.0b697cf4.ttf
│ │ ├── fa-regular-400.8c986198.woff
│ │ ├── fa-regular-400.e07d72d7.eot
│ │ ├── fa-solid-900.9c39a8a4.woff2
│ │ ├── Roboto-Regular.2751ee43.woff2
│ │ └── fa-regular-400.28ec6d38.woff2
│ └── js
│ │ ├── 0.9e19aec0.js
│ │ ├── 7.5635fa08.js
│ │ ├── 8.094ad6fb.js
│ │ └── 4.7a34dcbf.js
├── manifest.json
├── 404.html
└── about
│ └── index.html
├── .npmignore
├── README.md
├── .eslintrc.js
├── .editorconfig
├── LICENSE
├── package.json
└── bin
└── vuepress.js
/src/.vuepress/components/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/noop.js:
--------------------------------------------------------------------------------
1 | module.exports = {}
2 |
--------------------------------------------------------------------------------
/src/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: home
3 | ---
4 |
--------------------------------------------------------------------------------
/src/.vuepress/override.styl:
--------------------------------------------------------------------------------
1 | // override theme config
2 |
3 |
--------------------------------------------------------------------------------
/src/about/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: about
3 | title: 关于我
4 | ---
5 |
--------------------------------------------------------------------------------
/src/tags/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: tags
3 | title: Tags
4 | ---
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | *.log
4 | .temp
5 | vuepress
6 | TODOs.md
7 |
--------------------------------------------------------------------------------
/docs/face.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/face.jpg
--------------------------------------------------------------------------------
/docs/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/favicon.ico
--------------------------------------------------------------------------------
/docs/icons/192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/icons/192.png
--------------------------------------------------------------------------------
/docs/icons/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/icons/512.png
--------------------------------------------------------------------------------
/docs/icons/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/icons/favicon.png
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .temp
3 | vuepress
4 | TODOs.md
5 | art
6 | test
7 | docs
8 | .eslintrc.js
9 | yarn.lock
10 |
--------------------------------------------------------------------------------
/src/.vuepress/public/face.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/public/face.jpg
--------------------------------------------------------------------------------
/docs/assets/img/brand.734f817b.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/img/brand.734f817b.jpg
--------------------------------------------------------------------------------
/docs/assets/img/face.55c9d89d.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/img/face.55c9d89d.jpg
--------------------------------------------------------------------------------
/src/.vuepress/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/public/favicon.ico
--------------------------------------------------------------------------------
/src/.vuepress/public/icons/192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/public/icons/192.png
--------------------------------------------------------------------------------
/src/.vuepress/public/icons/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/public/icons/512.png
--------------------------------------------------------------------------------
/src/.vuepress/theme/imgs/brand.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/imgs/brand.jpg
--------------------------------------------------------------------------------
/src/.vuepress/public/icons/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/public/icons/favicon.png
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Bold.39b2c303.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Bold.39b2c303.woff2
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Bold.dc81817d.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Bold.dc81817d.woff
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Bold.e31fcf18.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Bold.e31fcf18.ttf
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Light.3b813c2a.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Light.3b813c2a.woff
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Light.46e48ce0.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Light.46e48ce0.ttf
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Medium.894a2ede.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Medium.894a2ede.ttf
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Thin.7500519d.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Thin.7500519d.woff
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Thin.94998475.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Thin.94998475.ttf
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Thin.954bbdeb.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Thin.954bbdeb.woff2
--------------------------------------------------------------------------------
/docs/assets/fonts/fa-brands-400.c601db56.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-brands-400.c601db56.ttf
--------------------------------------------------------------------------------
/docs/assets/fonts/fa-brands-400.e2a7835b.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-brands-400.e2a7835b.eot
--------------------------------------------------------------------------------
/docs/assets/fonts/fa-solid-900.24f9359f.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-solid-900.24f9359f.eot
--------------------------------------------------------------------------------
/docs/assets/fonts/fa-solid-900.4ff89f93.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-solid-900.4ff89f93.woff
--------------------------------------------------------------------------------
/docs/assets/fonts/fa-solid-900.af4698a4.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-solid-900.af4698a4.ttf
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vuepress-theme-indigo
2 |
3 | > 这个仓库是 [hexo-theme-indigo](https://github.com/yscoder/hexo-theme-indigo) 主题的 vuepress 版本。
4 |
5 | 开发中,敬请期待!
6 |
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Light.69f8a061.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Light.69f8a061.woff2
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Medium.574fd0b5.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Medium.574fd0b5.woff2
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Medium.fc78759e.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Medium.fc78759e.woff
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Regular.ba3dcd89.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Regular.ba3dcd89.woff
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Regular.df7b648c.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Regular.df7b648c.ttf
--------------------------------------------------------------------------------
/docs/assets/fonts/fa-brands-400.9404b3cb.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-brands-400.9404b3cb.woff2
--------------------------------------------------------------------------------
/docs/assets/fonts/fa-brands-400.cc6aff50.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-brands-400.cc6aff50.woff
--------------------------------------------------------------------------------
/docs/assets/fonts/fa-regular-400.0b697cf4.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-regular-400.0b697cf4.ttf
--------------------------------------------------------------------------------
/docs/assets/fonts/fa-regular-400.8c986198.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-regular-400.8c986198.woff
--------------------------------------------------------------------------------
/docs/assets/fonts/fa-regular-400.e07d72d7.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-regular-400.e07d72d7.eot
--------------------------------------------------------------------------------
/docs/assets/fonts/fa-solid-900.9c39a8a4.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-solid-900.9c39a8a4.woff2
--------------------------------------------------------------------------------
/docs/assets/fonts/Roboto-Regular.2751ee43.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/Roboto-Regular.2751ee43.woff2
--------------------------------------------------------------------------------
/docs/assets/fonts/fa-regular-400.28ec6d38.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/docs/assets/fonts/fa-regular-400.28ec6d38.woff2
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | exports.dev = require('./dev')
2 | exports.build = require('./build')
3 | exports.eject = require('./eject')
4 | Object.assign(exports, require('./util'))
5 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.eot
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.ttf
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.woff
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.eot
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.ttf
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.eot
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.ttf
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.woff
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Bold.woff2
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.woff
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Light.woff2
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.eot
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.ttf
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.woff
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Medium.woff2
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.eot
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.ttf
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.woff
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Thin.woff2
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: ['plugin:vue-libs/recommended'],
4 | rules: {
5 | indent: ['error', 2, { MemberExpression: 'off' }]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yscoder/vuepress-theme-indigo/HEAD/src/.vuepress/theme/styles/fonts/roboto/Roboto-Regular.woff2
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 |
10 | [*.{js}]
11 | indent_size = 4
12 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/About.vue:
--------------------------------------------------------------------------------
1 |
2 | 404
4 |
{{ getMsg() }}6 |
${info || defaultTitle}
\n` 23 | } else { 24 | return `${code}`
14 | }
15 |
16 | module.exports = (str, lang) => {
17 | if (!lang) {
18 | return wrap(str, 'text')
19 | }
20 | const rawLang = lang
21 | if (lang === 'vue' || lang === 'html') {
22 | lang = 'markup'
23 | }
24 | if (lang === 'md') {
25 | lang = 'markdown'
26 | }
27 | if (!prism.languages[lang]) {
28 | try {
29 | loadLanguages([lang])
30 | } catch (e) {
31 | console.log(chalk.yellow(`[vuepress] Syntax highlight for language "${lang}" is not supported.`))
32 | }
33 | }
34 | if (prism.languages[lang]) {
35 | const code = prism.highlight(str, prism.languages[lang], lang)
36 | return wrap(code, rawLang)
37 | }
38 | return wrap(str, 'text')
39 | }
40 |
--------------------------------------------------------------------------------
/lib/default-theme/styles/nprogress.styl:
--------------------------------------------------------------------------------
1 | #nprogress
2 | pointer-events none
3 | .bar
4 | background $accentColor
5 | position fixed
6 | z-index 1031
7 | top 0
8 | left 0
9 | width 100%
10 | height 2px
11 | .peg
12 | display block
13 | position absolute
14 | right 0px
15 | width 100px
16 | height 100%
17 | box-shadow 0 0 10px $accentColor, 0 0 5px $accentColor
18 | opacity 1.0
19 | transform rotate(3deg) translate(0px, -4px)
20 | .spinner
21 | display block
22 | position fixed
23 | z-index 1031
24 | top 15px
25 | right 15px
26 | .spinner-icon
27 | width 18px
28 | height 18px
29 | box-sizing border-box
30 | border solid 2px transparent
31 | border-top-color $accentColor
32 | border-left-color $accentColor
33 | border-radius 50%
34 | animation nprogress-spinner 400ms linear infinite
35 |
36 | .nprogress-custom-parent
37 | overflow hidden
38 | position relative
39 |
40 | .nprogress-custom-parent #nprogress .spinner,
41 | .nprogress-custom-parent #nprogress .bar
42 | position absolute
43 |
44 | @keyframes nprogress-spinner
45 | 0% transform rotate(0deg)
46 | 100% transform rotate(360deg)
47 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/config.styl:
--------------------------------------------------------------------------------
1 | $body-font-family = -apple-system,BlinkMacSystemFont,Roboto,"Segoe UI","Helvetica Neue",Helvetica,"PingFang SC", "Hiragino Sans GB","Microsoft YaHei",SimSun,sans-serif
2 | $font-size-root = 14px
3 | $line-height-root = 1.5
4 | $card-border-radius = 4px
5 |
6 | $font-roboto = './fonts/roboto'
7 | $font-code = Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace
8 |
9 | $container-max-width = 1000px
10 | $body-background = #f6f6f6
11 | $primary-color = #3F51B5
12 | $light-primary-color = lighten($primary-color, 50%)
13 | $content-color = #4d4d4d
14 | $gray-color = #7e7e7e
15 |
16 | $custom-blocks = {
17 | tip: #4caf50
18 | warning: #FF9800
19 | danger: #ff5722
20 | }
21 |
22 | $code-languages = {
23 | js: 'js' 'javascript'
24 | html: 'html' 'markup'
25 | md: 'md' 'markdown'
26 | vue: 'vue'
27 | css: 'css'
28 | less: 'less'
29 | sass: 'sass'
30 | scss: 'scss'
31 | stylus: 'stylus'
32 | jsx: 'jsx'
33 | ts: 'ts' 'typescript'
34 | json: 'json'
35 | yaml: 'yaml'
36 | bash: 'bash'
37 | c: 'c'
38 | java: 'java'
39 | kotlin: 'kotlin'
40 | python: 'py' 'python'
41 | csharp: 'c#' 'csharp'
42 | go: 'go'
43 | sql: 'sql'
44 | }
45 |
46 | @import '~@source/.vuepress/override.styl'
47 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/components/Pagination.vue:
--------------------------------------------------------------------------------
1 |
2 | /
5 |
6 | module.exports = md => {
7 | const fence = md.renderer.rules.fence
8 | md.renderer.rules.fence = (...args) => {
9 | const [tokens, idx, options] = args
10 | const token = tokens[idx]
11 |
12 | if (!token.info || !RE.test(token.info)) {
13 | return fence(...args)
14 | }
15 |
16 | const langName = token.info.replace(RE, '').trim()
17 | const lineNumbers = RE.exec(token.info)[1]
18 | .split(',')
19 | .map(v => v.split('-').map(v => parseInt(v, 10)))
20 |
21 | const code = options.highlight
22 | ? options.highlight(token.content, langName)
23 | : token.content
24 |
25 | const rawCode = code.replace(wrapperRE, '')
26 | const leadingWrapper = code.match(wrapperRE)[0]
27 |
28 | const codeSplits = rawCode.split('\n').map((split, index) => {
29 | const lineNumber = index + 1
30 | const inRange = lineNumbers.some(([start, end]) => {
31 | if (start && end) {
32 | return lineNumber >= start && lineNumber <= end
33 | }
34 | return lineNumber === start
35 | })
36 | if (inRange) {
37 | return {
38 | code: `${split}`,
39 | highlighted: true
40 | }
41 | }
42 | return {
43 | code: split
44 | }
45 | })
46 | let highlightedCode = leadingWrapper
47 | codeSplits.forEach(split => {
48 | if (split.highlighted) {
49 | highlightedCode += split.code
50 | } else {
51 | highlightedCode += `${split.code}\n`
52 | }
53 | })
54 | return highlightedCode
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/docs/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | VuePress
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | 404
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/Post.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
7 |
9 |
12 |
13 |
14 |
15 |
17 |
21 |
22 | {{$tt('postNav_prev')}}
23 |
24 |
25 |
26 |
28 |
32 | {{$tt('postNav_next')}}
33 |
34 |
35 |
36 |
37 |
38 |
40 |
41 |
42 |
43 |
44 |
45 |
56 |
71 |
--------------------------------------------------------------------------------
/lib/default-theme/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
11 | {{ $siteTitle }}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
40 |
41 |
72 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/Footer.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
11 |
12 | {{$site.themeConfig.author}} © {{since}}
13 |
14 |
15 |
18 | {{$site.themeConfig.icpLicense}}
19 |
20 |
21 | Power by
22 | VuePress Theme
25 | indigo
28 |
29 |
30 |
31 |
32 |
33 |
44 |
74 |
--------------------------------------------------------------------------------
/lib/markdown/link.js:
--------------------------------------------------------------------------------
1 | // markdown-it plugin for:
2 | // 1. adding target="_blank" to external links
3 | // 2. converting internal links to
4 |
5 | module.exports = md => {
6 | let hasOpenRouterLink = false
7 |
8 | md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
9 | const token = tokens[idx]
10 | const hrefIndex = token.attrIndex('href')
11 | if (hrefIndex >= 0) {
12 | const link = token.attrs[hrefIndex]
13 | const href = link[1]
14 | const isExternal = /^https?:/.test(href)
15 | const isSourceLink = /(\/|\.md|\.html)(#.*)?$/.test(href)
16 | if (isExternal) {
17 | addAttr(token, 'target', '_blank')
18 | addAttr(token, 'rel', 'noopener noreferrer')
19 | } else if (isSourceLink) {
20 | hasOpenRouterLink = true
21 | tokens[idx] = toRouterLink(token, link)
22 | }
23 | }
24 | return self.renderToken(tokens, idx, options)
25 | }
26 |
27 | function toRouterLink (token, link) {
28 | link[0] = 'to'
29 | let to = link[1]
30 |
31 | // convert link to filename and export it for existence check
32 | const links = md.__data.links || (md.__data.links = [])
33 | links.push(to)
34 |
35 | to = to
36 | .replace(/\.md$/, '.html')
37 | .replace(/\.md(#.*)$/, '.html$1')
38 | // normalize links to README/index
39 | if (/^index|readme\.html/i.test(to)) {
40 | to = '/'
41 | }
42 | // markdown-it encodes the uri
43 | link[1] = decodeURI(to)
44 | return Object.assign({}, token, {
45 | tag: 'router-link'
46 | })
47 | }
48 |
49 | md.renderer.rules.link_close = (tokens, idx, options, env, self) => {
50 | const token = tokens[idx]
51 | if (hasOpenRouterLink) {
52 | token.tag = 'router-link'
53 | hasOpenRouterLink = false
54 | }
55 | return self.renderToken(tokens, idx, options)
56 | }
57 | }
58 |
59 | function addAttr (token, name, val) {
60 | const targetIndex = token.attrIndex(name)
61 | if (targetIndex < 0) {
62 | token.attrPush([name, val])
63 | } else {
64 | token.attrs[targetIndex][1] = val
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/default-theme/styles/code.styl:
--------------------------------------------------------------------------------
1 | @require './config'
2 |
3 | .content
4 | code
5 | color lighten($textColor, 20%)
6 | padding 0.25rem 0.5rem
7 | margin 0
8 | font-size 0.85em
9 | background-color rgba(27,31,35,0.05)
10 | border-radius 3px
11 |
12 | .content
13 | pre, pre[class*="language-"]
14 | background-color $codeBgColor
15 | line-height 1.4
16 | border-radius 6px
17 | padding 1.25rem 1.5rem
18 | margin 0.85rem 0
19 | white-space pre-wrap
20 | word-break break-word
21 | overflow auto
22 | position relative
23 | code
24 | color #fff
25 | padding 0
26 | background-color transparent
27 | border-radius 0
28 | &:before
29 | position absolute
30 | top 0.8em
31 | right 1em
32 | font-size 0.75rem
33 | color rgba(255, 255, 255, 0.4)
34 | .highlighted-line
35 | background-color rgba(0, 0, 0, 66%)
36 | display block
37 | margin 0 -1.5rem
38 | padding 0 1.5rem
39 |
40 | pre[class="language-js"], pre[class="language-javascript"]
41 | &:before
42 | content "js"
43 |
44 | pre[class="language-html"], pre[class="language-markup"]
45 | &:before
46 | content "html"
47 |
48 | pre[class="language-markdown"], pre[class="language-md"]
49 | &:before
50 | content "md"
51 |
52 | pre[class="language-vue"]:before
53 | content "vue"
54 |
55 | pre[class="language-css"]:before
56 | content "css"
57 |
58 | pre[class="language-sass"]:before
59 | content "sass"
60 |
61 | pre[class="language-less"]:before
62 | content "less"
63 |
64 | pre[class="language-scss"]:before
65 | content "scss"
66 |
67 | pre[class="language-stylus"]:before
68 | content "stylus"
69 |
70 | pre[class="language-json"]:before
71 | content "json"
72 |
73 | pre[class="language-ruby"]:before
74 | content "rb"
75 |
76 | pre[class="language-python"]:before
77 | content "py"
78 |
79 | pre[class="language-go"]:before
80 | content "go"
81 |
82 | pre[class="language-java"]:before
83 | content "java"
84 |
85 | pre[class="language-c"]:before
86 | content "c"
87 |
88 | pre[class="language-bash"]:before
89 | content "sh"
90 |
--------------------------------------------------------------------------------
/src/posts/test-markdown.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 测试 Markdown
3 | date: 2018-04-29 12:15:53
4 | tags: [markdown]
5 | ---
6 |
7 | 常用 Markdown 语法测试。
8 |
9 |
10 |
11 | ### 1. 斜体和粗体
12 |
13 | 使用 \* 和 \*\* 表示斜体和粗体。
14 |
15 | 示例:
16 |
17 | 这是 _斜体_,这是 **粗体**。
18 |
19 | ### 2. 分级标题
20 |
21 | 使用 === 表示一级标题,使用 --- 表示二级标题。
22 |
23 | 示例:
24 |
25 | ```
26 | 这是一个一级标题
27 | ============================
28 |
29 | 这是一个二级标题
30 | --------------------------------------------------
31 |
32 | ### 这是一个三级标题
33 | ```
34 |
35 | 你也可以选择在行首加井号表示不同级别的标题 (H1-H6),例如:# H1, ## H2, ### H3,#### H4。
36 |
37 | ### 3. 外链接
38 |
39 | 使用 \[描述](链接地址) 为文字增加外链接。
40 |
41 | 示例:
42 |
43 | 这是去往 [本人博客](/) 的链接。
44 |
45 | ### 4. 无序列表
46 |
47 | 使用 \*,+,- 表示无序列表。
48 |
49 | 示例:
50 |
51 | * 无序列表项 一
52 | * 无序列表项 二
53 | * 无序列表项 三
54 |
55 | ### 5. 有序列表
56 |
57 | 使用数字和点表示有序列表。
58 |
59 | 示例:
60 |
61 | 1. 有序列表项 一
62 | 2. 有序列表项 二
63 | 3. 有序列表项 三
64 |
65 | ### 6. 文字引用
66 |
67 | 使用 > 表示文字引用。
68 |
69 | 示例:
70 |
71 | > 野火烧不尽,春风吹又生。
72 |
73 | ### 7. 行内代码块
74 |
75 | 使用 \`代码` 表示行内代码块。
76 |
77 | 示例:
78 |
79 | 让我们聊聊 `html`。
80 |
81 | ### 8. 代码块
82 |
83 | 使用 四个缩进空格 表示代码块。
84 |
85 | 示例:
86 |
87 | 这是一个代码块,此行左侧有四个不可见的空格。
88 |
89 | ```js{4}
90 | export default {
91 | data () {
92 | return {
93 | msg: 'Highlighted!'
94 | }
95 | }
96 | }
97 | ```
98 |
99 | ### 9. 插入图像
100 |
101 | 使用 \!\[描述](图片链接地址) 插入图像。
102 |
103 | 示例:
104 |
105 | 
106 |
107 | ### 10. 删除线
108 |
109 | 使用 ~~ 表示删除线。
110 |
111 | ~~这是一段错误的文本。~~
112 |
113 | ### 11. 表格
114 |
115 | | 项目 | 价格 | 数量 |
116 | | ------ | -----: | :--: |
117 | | 计算机 | \$1600 | 5 |
118 | | 手机 | \$12 | 12 |
119 | | 管线 | \$1 | 234 |
120 |
121 | ### 12. 自定义容器
122 |
123 | **Input**
124 |
125 | ```
126 | ::: tip 提示
127 | This is a tip
128 | :::
129 |
130 | ::: warning 注意
131 | This is a warning
132 | :::
133 |
134 | ::: danger 警告
135 | This is a dangerous warning
136 | :::
137 | ```
138 |
139 | **Output**
140 |
141 | ::: tip 提示
142 | This is a tip
143 | :::
144 |
145 | ::: warning 注意
146 | This is a warning
147 | :::
148 |
149 | ::: danger 警告
150 | This is a dangerous warning
151 | :::
152 |
--------------------------------------------------------------------------------
/lib/app/clientEntry.js:
--------------------------------------------------------------------------------
1 | /* global BASE_URL, GA_ID, ga, SW_ENABLED */
2 |
3 | import '@temp/polyfill'
4 | import { createApp } from './app'
5 | import { register } from 'register-service-worker'
6 |
7 | const { app, router } = createApp()
8 |
9 | // Google analytics integration
10 | if (process.env.NODE_ENV === 'production' && GA_ID) {
11 | (function (i, s, o, g, r, a, m) {
12 | i['GoogleAnalyticsObject'] = r
13 | i[r] = i[r] || function () {
14 | (i[r].q = i[r].q || []).push(arguments)
15 | }
16 | i[r].l = 1 * new Date()
17 | a = s.createElement(o)
18 | m = s.getElementsByTagName(o)[0]
19 | a.async = 1
20 | a.src = g
21 | m.parentNode.insertBefore(a, m)
22 | })(window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga')
23 |
24 | ga('create', GA_ID, 'auto')
25 | ga('send', 'pageview')
26 |
27 | router.afterEach(function (to) {
28 | ga('set', 'page', to.fullPath)
29 | ga('send', 'pageview')
30 | })
31 | }
32 |
33 | router.onReady(() => {
34 | app.$mount('#app')
35 |
36 | // Register service worker
37 | if (process.env.NODE_ENV === 'production' &&
38 | SW_ENABLED &&
39 | window.location.protocol === 'https:') {
40 | register(`${BASE_URL}service-worker.js`, {
41 | ready () {
42 | console.log('[vuepress:sw] Service worker is active.')
43 | app.$refs.layout.$emit('sw-ready')
44 | },
45 | cached () {
46 | console.log('[vuepress:sw] Content has been cached for offline use.')
47 | app.$refs.layout.$emit('sw-cached')
48 | },
49 | updated () {
50 | console.log('[vuepress:sw] Content updated.')
51 | app.$refs.layout.$emit('sw-updated')
52 | },
53 | offline () {
54 | console.log('[vuepress:sw] No internet connection found. App is running in offline mode.')
55 | app.$refs.layout.$emit('sw-offline')
56 | },
57 | error (err) {
58 | console.error('[vuepress:sw] Error during service worker registration:', err)
59 | app.$refs.layout.$emit('sw-error', err)
60 | if (GA_ID) {
61 | ga('send', 'exception', {
62 | exDescription: err.message,
63 | exFatal: false
64 | })
65 | }
66 | }
67 | })
68 | }
69 | })
70 |
--------------------------------------------------------------------------------
/lib/webpack/createClientConfig.js:
--------------------------------------------------------------------------------
1 | module.exports = function createClientConfig (options, cliOptions) {
2 | const path = require('path')
3 | const WebpackBar = require('webpackbar')
4 | const createBaseConfig = require('./createBaseConfig')
5 |
6 | const config = createBaseConfig(options, cliOptions)
7 |
8 | config
9 | .entry('app')
10 | .add(path.resolve(__dirname, '../app/clientEntry.js'))
11 |
12 | config.node
13 | .merge({
14 | // prevent webpack from injecting useless setImmediate polyfill because Vue
15 | // source contains it (although only uses it if it's native).
16 | setImmediate: false,
17 | global: false,
18 | process: false,
19 | // prevent webpack from injecting mocks to Node native modules
20 | // that does not make sense for the client
21 | dgram: 'empty',
22 | fs: 'empty',
23 | net: 'empty',
24 | tls: 'empty',
25 | child_process: 'empty'
26 | })
27 |
28 | // generate client manifest only during build
29 | if (process.env.NODE_ENV === 'production') {
30 | // This is a temp build of vue-server-renderer/client-plugin.
31 | // TODO Switch back to original after problems are resolved.
32 | // Fixes two things:
33 | // 1. Include CSS in preload files
34 | // 2. filter out useless styles.xxxxx.js chunk from mini-css-extract-plugin
35 | // https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85
36 | config
37 | .plugin('ssr-client')
38 | .use(require('./ClientPlugin'), [{
39 | filename: 'manifest/client.json'
40 | }])
41 |
42 | config
43 | .plugin('optimize-css')
44 | .use(require('optimize-css-assets-webpack-plugin'), [{
45 | canPrint: false,
46 | cssProcessorOptions: {
47 | safe: true,
48 | autoprefixer: { disable: true },
49 | mergeLonghand: false
50 | }
51 | }])
52 | }
53 |
54 | if (!cliOptions.debug) {
55 | config
56 | .plugin('bar')
57 | .use(WebpackBar, [{
58 | name: 'Client',
59 | color: '#41b883',
60 | compiledIn: false
61 | }])
62 | }
63 |
64 | if (options.siteConfig.chainWebpack) {
65 | options.siteConfig.chainWebpack(config, false /* isServer */)
66 | }
67 |
68 | return config
69 | }
70 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Yusen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 | The MIT License (MIT)
24 |
25 | Copyright (c) 2018-present, Yuxi (Evan) You
26 |
27 | Permission is hereby granted, free of charge, to any person obtaining a copy
28 | of this software and associated documentation files (the "Software"), to deal
29 | in the Software without restriction, including without limitation the rights
30 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31 | copies of the Software, and to permit persons to whom the Software is
32 | furnished to do so, subject to the following conditions:
33 |
34 | The above copyright notice and this permission notice shall be included in
35 | all copies or substantial portions of the Software.
36 |
37 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
43 | THE SOFTWARE.
44 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/libs/blog.js:
--------------------------------------------------------------------------------
1 | import sortBy from 'lodash/sortBy'
2 | import dayjs from 'dayjs'
3 |
4 | const slugReg = /\/([^\/]+).html$/
5 | function matchSlug(path) {
6 | const arr = path.match(slugReg)
7 | return arr ? arr[1] : null
8 | }
9 |
10 | const install = (Vue, { theme, pages }) => {
11 | // 不依赖已有的内置数据,在这里对 siteData 解析,组装博客需要的数据混入Vue
12 | // Example: { postList: [], posts: {}, tagList: [], tags: { } }
13 | const postList = []
14 | const posts = {}
15 | const postDir = theme.postDir
16 |
17 | sortBy(pages, page => -new Date(page.frontmatter.date)).forEach(page => {
18 | if (page.path.indexOf(postDir) === 0) {
19 | const slug = matchSlug(page.path)
20 | postList.push(slug)
21 | posts[slug] = { ...page, slug }
22 | }
23 | })
24 |
25 | const tags = {}
26 | const tagList = []
27 | postList.forEach(slug => {
28 | const list = posts[slug].frontmatter.tags || []
29 | list.forEach(tagName => {
30 | if (!tags[tagName]) {
31 | tags[tagName] = []
32 | tagList.push(tagName)
33 | }
34 | tags[tagName] = tags[tagName].concat(slug)
35 | })
36 | })
37 |
38 | Vue.mixin({
39 | computed: {
40 | $blog() {
41 | return { postList, posts, tags, tagList }
42 | },
43 | $postNav() {
44 | const slug = matchSlug(this.$route.path)
45 | if (!slug) return
46 | const index = postList.indexOf(slug)
47 | const prev = postList[index - 1]
48 | const next = postList[index + 1]
49 | return {
50 | prev: prev ? posts[prev] : null,
51 | next: next ? posts[next] : null
52 | }
53 | },
54 | $page() {
55 | // override $page data
56 | const { path, meta } = this.$route
57 | for (let i = 0; i < pages.length; i++) {
58 | const page = pages[i]
59 | const layout = page.frontmatter.layout || 'post'
60 | if (page.path === path || layout === meta.layout) {
61 | return { ...page, path } // rewrite path
62 | }
63 | }
64 | }
65 | }
66 | })
67 |
68 | const format = theme.format
69 | Vue.filter('date', value => dayjs(value).format(format.date))
70 | Vue.filter('dateTime', value => dayjs(value).format(format.dateTime))
71 | }
72 |
73 | export default { install }
74 |
--------------------------------------------------------------------------------
/lib/app/dataMixin.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import { siteData } from './.temp/siteData'
3 | import { findPageForPath } from './util'
4 |
5 | function prepare (siteData) {
6 | siteData.pages.forEach(page => {
7 | if (!page.frontmatter) {
8 | page.frontmatter = {}
9 | }
10 | })
11 | if (siteData.locales) {
12 | Object.keys(siteData.locales).forEach(path => {
13 | siteData.locales[path].path = path
14 | })
15 | }
16 | Object.freeze(siteData)
17 | }
18 |
19 | prepare(siteData)
20 | const store = new Vue({
21 | data: { siteData }
22 | })
23 |
24 | if (module.hot) {
25 | module.hot.accept('./.temp/siteData', () => {
26 | prepare(siteData)
27 | store.siteData = siteData
28 | })
29 | }
30 |
31 | export default {
32 | computed: {
33 | $site () {
34 | return store.siteData
35 | },
36 | $localeConfig () {
37 | const { locales = {}} = this.$site
38 | let targetLang
39 | let defaultLang
40 | for (const path in locales) {
41 | if (path === '/') {
42 | defaultLang = locales[path]
43 | } else if (this.$page.path.indexOf(path) === 0) {
44 | targetLang = locales[path]
45 | }
46 | }
47 | return targetLang || defaultLang || {}
48 | },
49 | $siteTitle () {
50 | return this.$localeConfig.title || this.$site.title || ''
51 | },
52 | $title () {
53 | const page = this.$page
54 | const siteTitle = this.$siteTitle
55 | const selfTitle = page.frontmatter.home ? null : (
56 | page.frontmatter.title || // explicit title
57 | page.title // inferred title
58 | )
59 | return siteTitle
60 | ? selfTitle
61 | ? (siteTitle + ' | ' + selfTitle)
62 | : siteTitle
63 | : selfTitle || 'VuePress'
64 | },
65 | $description () {
66 | return this.$page.frontmatter.description || this.$localeConfig.description || this.$site.description || ''
67 | },
68 | $lang () {
69 | return this.$page.frontmatter.lang || this.$localeConfig.lang || 'en-US'
70 | },
71 | $localePath () {
72 | return this.$localeConfig.path || '/'
73 | },
74 | $themeLocaleConfig () {
75 | return (this.$site.themeConfig.locales || {})[this.$localePath] || {}
76 | },
77 | $page () {
78 | return findPageForPath(
79 | this.$site.pages,
80 | this.$route.path
81 | )
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/lib/util/index.js:
--------------------------------------------------------------------------------
1 | const parseEmojis = str => {
2 | const emojiData = require('markdown-it-emoji/lib/data/full.json')
3 | return str.replace(/:(.+?):/g, (placeholder, key) => emojiData[key] || placeholder)
4 | }
5 |
6 | exports.normalizeHeadTag = tag => {
7 | if (typeof tag === 'string') {
8 | tag = [tag]
9 | }
10 | const tagName = tag[0]
11 | return {
12 | tagName,
13 | attributes: tag[1] || {},
14 | innerHTML: tag[2] || '',
15 | closeTag: !(tagName === 'meta' || tagName === 'link')
16 | }
17 | }
18 |
19 | exports.applyUserWebpackConfig = function (userConfig, config, isServer) {
20 | const merge = require('webpack-merge')
21 | if (typeof userConfig === 'object') {
22 | return merge(config, userConfig)
23 | }
24 | if (typeof userConfig === 'function') {
25 | const res = userConfig(config, isServer)
26 | if (res && typeof res === 'object') {
27 | return merge(config, res)
28 | }
29 | }
30 | return config
31 | }
32 |
33 | exports.inferTitle = function (frontmatter) {
34 | if (frontmatter.data.home) {
35 | return 'Home'
36 | }
37 | if (frontmatter.data.title) {
38 | return parseEmojis(frontmatter.data.title)
39 | }
40 | const match = frontmatter.content.trim().match(/^#+\s+(.*)/)
41 | if (match) {
42 | return parseEmojis(match[1])
43 | }
44 | }
45 |
46 | exports.parseFrontmatter = content => {
47 | const matter = require('gray-matter')
48 | const toml = require('toml')
49 |
50 | return matter(content, {
51 | excerpt_separator: '',
52 | engines: {
53 | toml: toml.parse.bind(toml),
54 | excerpt: false
55 | }
56 | })
57 | }
58 |
59 | const LRU = require('lru-cache')
60 | const cache = LRU({ max: 1000 })
61 |
62 | exports.extractHeaders = (content, include = [], md) => {
63 | const key = content + include.join(',')
64 | const hit = cache.get(key)
65 | if (hit) {
66 | return hit
67 | }
68 |
69 | const tokens = md.parse(content, {})
70 |
71 | const res = []
72 | tokens.forEach((t, i) => {
73 | if (t.type === 'heading_open' && include.includes(t.tag)) {
74 | const title = parseEmojis(tokens[i + 1].content)
75 | const slug = t.attrs.find(([name]) => name === 'id')[1]
76 | res.push({
77 | level: parseInt(t.tag.slice(1), 10),
78 | title,
79 | slug: slug || md.slugify(title)
80 | })
81 | }
82 | })
83 |
84 | cache.set(key, res)
85 | return res
86 | }
87 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/components/Share.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
17 |
18 |
20 |
21 | {{$tt(item.text)}}
22 |
23 |
27 |
28 |
29 |
30 | {{$tt('copyLink')}}
31 |
32 |
33 |
39 |
40 |
41 |
84 |
--------------------------------------------------------------------------------
/lib/app/app.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import Content from './Content'
4 | import ClientOnly from './ClientOnly'
5 | import dataMixin from './dataMixin'
6 | import NotFound from '@notFound'
7 | import { routes } from '@temp/routes'
8 | import { siteData } from '@temp/siteData'
9 | import enhanceApp from '@temp/enhanceApp'
10 | import themeEnhanceApp from '@temp/themeEnhanceApp'
11 |
12 | // suggest dev server restart on base change
13 | if (module.hot) {
14 | const prevBase = siteData.base
15 | module.hot.accept('./.temp/siteData', () => {
16 | if (siteData.base !== prevBase) {
17 | window.alert(
18 | `[vuepress] Site base has changed. ` +
19 | `Please restart dev server to ensure correct asset paths.`
20 | )
21 | }
22 | })
23 | }
24 |
25 | Vue.config.productionTip = false
26 | Vue.use(Router)
27 | // mixin for exposing $site and $page
28 | Vue.mixin(dataMixin)
29 | // component for rendering markdown content and setting title etc.
30 | Vue.component('Content', Content)
31 | // component for client-only content
32 | Vue.component('ClientOnly', ClientOnly)
33 |
34 | // global helper for adding base path to absolute urls
35 | Vue.prototype.$withBase = function (path) {
36 | const base = this.$site.base
37 | if (path.charAt(0) === '/') {
38 | return base + path.slice(1)
39 | } else {
40 | return path
41 | }
42 | }
43 |
44 | // add 404 route
45 | routes.push({
46 | path: '*',
47 | component: NotFound
48 | })
49 |
50 | export function createApp () {
51 | const router = new Router({
52 | base: siteData.base,
53 | mode: 'history',
54 | fallback: false,
55 | routes,
56 | scrollBehavior: (to, from, saved) => {
57 | if (saved) {
58 | return saved
59 | } else if (to.hash) {
60 | return { selector: to.hash }
61 | } else {
62 | return { x: 0, y: 0 }
63 | }
64 | }
65 | })
66 |
67 | // redirect /foo to /foo/
68 | router.beforeEach((to, from, next) => {
69 | if (!/(\/|\.html)$/.test(to.path)) {
70 | next(Object.assign({}, to, {
71 | path: to.path + '/'
72 | }))
73 | } else {
74 | next()
75 | }
76 | })
77 |
78 | const options = {}
79 |
80 | themeEnhanceApp({ Vue, options, router, siteData })
81 | enhanceApp({ Vue, options, router, siteData })
82 |
83 | const app = new Vue(
84 | Object.assign(options, {
85 | router,
86 | render (h) {
87 | return h('div', { attrs: { id: 'app' }}, [
88 | h('router-view', { ref: 'layout' })
89 | ])
90 | }
91 | })
92 | )
93 |
94 | return { app, router }
95 | }
96 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/components/PostCard.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 | {{page.title}}
11 | {{page.title}}
13 |
16 |
17 |
18 |
19 |
20 | {{page.excerpt}}
21 |
22 |
23 |
24 |
25 | {{tag}}
28 |
29 |
30 |
31 |
32 |
68 |
107 |
--------------------------------------------------------------------------------
/docs/assets/js/4.7a34dcbf.js:
--------------------------------------------------------------------------------
1 | (window.webpackJsonp=window.webpackJsonp||[]).push([[4],{358:function(t,s,a){"use strict";a.r(s);var e=a(0),n=Object(e.a)({},function(){this.$createElement;this._self._c;return this._m(0)},[function(){var t=this,s=t.$createElement,a=t._self._c||s;return a("div",{staticClass:"content"},[a("p",[t._v("关于前端页面的文字溢出截断的招数已经很常见了。\n通常的实现有,前端css控制、后端字数输出控制或者前端js字数处理等。")]),a("h2",{attrs:{id:"单行文字"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#单行文字","aria-hidden":"true"}},[t._v("#")]),t._v(" 单行文字")]),a("p",[t._v("单行文字的溢出处理很简单,我通常是使用css来控制,在文字末尾加上"),a("code",[t._v("...")]),t._v("。")]),a("pre",{pre:!0,attrs:{class:"language-css"}},[a("code",[a("span",{attrs:{class:"token selector"}},[t._v(".ellipsis")]),t._v(" "),a("span",{attrs:{class:"token punctuation"}},[t._v("{")]),t._v("\n "),a("span",{attrs:{class:"token property"}},[t._v("overflow")]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" hidden"),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n "),a("span",{attrs:{class:"token property"}},[t._v("text-overflow")]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" ellipsis"),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n "),a("span",{attrs:{class:"token property"}},[t._v("white-space")]),a("span",{attrs:{class:"token punctuation"}},[t._v(":")]),t._v(" nowrap"),a("span",{attrs:{class:"token punctuation"}},[t._v(";")]),t._v("\n"),a("span",{attrs:{class:"token punctuation"}},[t._v("}")]),t._v("\n")])]),a("p",[t._v("给需要文字截断的节点增加一个这样的基础类,然后设置该节点的宽。\n同时注意给未来可能会出现溢出的节点也加上此类,再设置最大宽度"),a("code",[t._v("max-width")]),t._v(",因为很多奇葩用户的输入是你无法掌控的。 - -!")]),a("h2",{attrs:{id:"多行文字"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#多行文字","aria-hidden":"true"}},[t._v("#")]),t._v(" 多行文字")]),a("p",[t._v("我希望在一个的固定高度的容器中,内容超出后,最后一个文字显示"),a("code",[t._v("...")]),t._v("。")]),a("p",[t._v("如法炮制给多行文字的容器添加"),a("code",[t._v("ellipsis")]),t._v("类后,你会发现的确是显示"),a("code",[t._v("...")]),t._v(",不过此时文字是一行的。\n因为在该类中添加了属性"),a("code",[t._v("white-space")]),t._v(",用来定义一个段落如何换行。属性值"),a("code",[t._v("nowrap")]),t._v(":禁止文本换行,除非遇到"),a("code",[t._v("
")]),t._v("。")]),a("p",[t._v("一番思考后,确定了一个方案。")]),a("ol",[a("li",[t._v("给固定高度的容器添加"),a("code",[t._v("overflow: hidden")]),t._v(";")]),a("li",[t._v("给容器添加相对定位;")]),a("li",[t._v("添加伪元素样式,"),a("code",[t._v("content:'...'")]),t._v(",绝对定位,然后位置定位在容器末尾。")])]),a("p",[t._v("伪元素的兼容性为IE8,如果需要兼容IE7的可以使用标签代替。")]),a("h2",{attrs:{id:"带显示全部的多行文字"}},[a("a",{staticClass:"header-anchor",attrs:{href:"#带显示全部的多行文字","aria-hidden":"true"}},[t._v("#")]),t._v(" 带显示全部的多行文字")]),a("p",[t._v("类似 QQ 空间、微信、微博那种。")]),a("p",[t._v("{% jsfiddle imys/wymxhaek/3 html,css,result %}")]),a("p",[t._v("使用 2 个伪元素加 1 个 a 链接实现。\n精妙之处在于使用伪元素遮挡一行文字,让 a 链接位于伪元素之上显示。")])])}],!1,null,null,null);s.default=n.exports}}]);
--------------------------------------------------------------------------------
/lib/default-theme/Sidebar.vue:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 |
20 |
71 |
72 |
103 |
--------------------------------------------------------------------------------
/lib/markdown/component.js:
--------------------------------------------------------------------------------
1 | // Replacing the default htmlBlock rule to allow using custom components at
2 | // root level
3 |
4 | const blockNames = require('markdown-it/lib/common/html_blocks')
5 | const HTML_OPEN_CLOSE_TAG_RE = require('markdown-it/lib/common/html_re').HTML_OPEN_CLOSE_TAG_RE
6 |
7 | // An array of opening and corresponding closing sequences for html tags,
8 | // last argument defines whether it can terminate a paragraph or not
9 | const HTML_SEQUENCES = [
10 | [/^<(script|pre|style)(?=(\s|>|$))/i, /<\/(script|pre|style)>/i, true],
11 | [/^/, true],
12 | [/^<\?/, /\?>/, true],
13 | [/^/, true],
14 | [/^/, true],
15 | // PascalCase Components
16 | [/^<[A-Z]/, />/, true],
17 | // custom elements with hyphens
18 | [/^<\w+\-/, />/, true],
19 | [new RegExp('^?(' + blockNames.join('|') + ')(?=(\\s|/?>|$))', 'i'), /^$/, true],
20 | [new RegExp(HTML_OPEN_CLOSE_TAG_RE.source + '\\s*$'), /^$/, false]
21 | ]
22 |
23 | module.exports = md => {
24 | md.block.ruler.at('html_block', htmlBlock)
25 | }
26 |
27 | function htmlBlock (state, startLine, endLine, silent) {
28 | let i, nextLine, lineText
29 | let pos = state.bMarks[startLine] + state.tShift[startLine]
30 | let max = state.eMarks[startLine]
31 |
32 | // if it's indented more than 3 spaces, it should be a code block
33 | if (state.sCount[startLine] - state.blkIndent >= 4) { return false }
34 |
35 | if (!state.md.options.html) { return false }
36 |
37 | if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false }
38 |
39 | lineText = state.src.slice(pos, max)
40 |
41 | for (i = 0; i < HTML_SEQUENCES.length; i++) {
42 | if (HTML_SEQUENCES[i][0].test(lineText)) { break }
43 | }
44 |
45 | if (i === HTML_SEQUENCES.length) {
46 | console.log(lineText)
47 | return false
48 | }
49 |
50 | if (silent) {
51 | // true if this sequence can be a terminator, false otherwise
52 | return HTML_SEQUENCES[i][2]
53 | }
54 |
55 | nextLine = startLine + 1
56 |
57 | // If we are here - we detected HTML block.
58 | // Let's roll down till block end.
59 | if (!HTML_SEQUENCES[i][1].test(lineText)) {
60 | for (; nextLine < endLine; nextLine++) {
61 | if (state.sCount[nextLine] < state.blkIndent) { break }
62 |
63 | pos = state.bMarks[nextLine] + state.tShift[nextLine]
64 | max = state.eMarks[nextLine]
65 | lineText = state.src.slice(pos, max)
66 |
67 | if (HTML_SEQUENCES[i][1].test(lineText)) {
68 | if (lineText.length !== 0) { nextLine++ }
69 | break
70 | }
71 | }
72 | }
73 |
74 | state.line = nextLine
75 |
76 | const token = state.push('html_block', '', 0)
77 | token.map = [startLine, nextLine]
78 | token.content = state.getLines(startLine, nextLine, state.blkIndent, true)
79 |
80 | return true
81 | }
82 |
--------------------------------------------------------------------------------
/lib/default-theme/SidebarLink.vue:
--------------------------------------------------------------------------------
1 |
57 |
58 |
89 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/SideNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
20 |
29 |
30 |
31 |
32 | {{item.text}}
33 |
34 |
35 |
36 |
37 |
47 |
117 |
--------------------------------------------------------------------------------
/lib/webpack/ClientPlugin.js:
--------------------------------------------------------------------------------
1 | // Temporarily hacked dev build
2 |
3 | var isJS = function (file) { return /\.js(\?[^.]+)?$/.test(file) }
4 |
5 | var isCSS = function (file) { return /\.css(\?[^.]+)?$/.test(file) }
6 |
7 | var onEmit = function (compiler, name, hook) {
8 | if (compiler.hooks) {
9 | // Webpack >= 4.0.0
10 | compiler.hooks.emit.tapAsync(name, hook)
11 | } else {
12 | // Webpack < 4.0.0
13 | compiler.plugin('emit', hook)
14 | }
15 | }
16 |
17 | var hash = require('hash-sum')
18 | var uniq = require('lodash.uniq')
19 | var VueSSRClientPlugin = function VueSSRClientPlugin (options) {
20 | if (options === void 0) options = {}
21 |
22 | this.options = Object.assign({
23 | filename: 'vue-ssr-client-manifest.json'
24 | }, options)
25 | }
26 |
27 | VueSSRClientPlugin.prototype.apply = function apply (compiler) {
28 | var this$1 = this
29 |
30 | onEmit(compiler, 'vue-client-plugin', function (compilation, cb) {
31 | var stats = compilation.getStats().toJson()
32 |
33 | var allFiles = uniq(stats.assets
34 | .map(function (a) { return a.name }))
35 | // Avoid preloading / injecting the style chunk
36 | .filter(file => !/styles\.\w{8}\.js$/.test(file))
37 |
38 | var initialFiles = uniq(Object.keys(stats.entrypoints)
39 | .map(function (name) { return stats.entrypoints[name].assets })
40 | .reduce(function (assets, all) { return all.concat(assets) }, [])
41 | .filter(function (file) { return isJS(file) || isCSS(file) }))
42 | // Avoid preloading / injecting the style chunk
43 | .filter(file => !/styles\.\w{8}\.js$/.test(file))
44 |
45 | var asyncFiles = allFiles
46 | .filter(function (file) { return isJS(file) || isCSS(file) })
47 | .filter(function (file) { return initialFiles.indexOf(file) < 0 })
48 |
49 | var manifest = {
50 | publicPath: stats.publicPath,
51 | all: allFiles,
52 | initial: initialFiles,
53 | async: asyncFiles,
54 | modules: { /* [identifier: string]: Array */ }
55 | }
56 |
57 | var assetModules = stats.modules.filter(function (m) { return m.assets.length })
58 | var fileToIndex = function (file) { return manifest.all.indexOf(file) }
59 | stats.modules.forEach(function (m) {
60 | // ignore modules duplicated in multiple chunks
61 | if (m.chunks.length === 1) {
62 | var cid = m.chunks[0]
63 | var chunk = stats.chunks.find(function (c) { return c.id === cid })
64 | if (!chunk || !chunk.files) {
65 | return
66 | }
67 | var id = m.identifier.replace(/\s\w+$/, '') // remove appended hash
68 | var files = manifest.modules[hash(id)] = chunk.files.map(fileToIndex)
69 | // find all asset modules associated with the same chunk
70 | assetModules.forEach(function (m) {
71 | if (m.chunks.some(function (id) { return id === cid })) {
72 | files.push.apply(files, m.assets.map(fileToIndex))
73 | }
74 | })
75 | }
76 | })
77 |
78 | var json = JSON.stringify(manifest, null, 2)
79 | compilation.assets[this$1.options.filename] = {
80 | source: function () { return json },
81 | size: function () { return json.length }
82 | }
83 | cb()
84 | })
85 | }
86 |
87 | module.exports = VueSSRClientPlugin
88 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vuepress-theme-indigo",
3 | "version": "0.0.1",
4 | "description": "vuepress theme indigo.",
5 | "main": "lib/index.js",
6 | "bin": {
7 | "vuepress": "bin/vuepress.js"
8 | },
9 | "scripts": {
10 | "dev": "node bin/vuepress dev src",
11 | "build": "node bin/vuepress build src",
12 | "lint": "eslint bin lib test",
13 | "start": "yarn dev && start http://localhost:8080"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/yscoder/vuepress-theme-indigo.git"
18 | },
19 | "keywords": [
20 | "vuepress-theme",
21 | "VuePress",
22 | "blog-theme",
23 | "Material Design"
24 | ],
25 | "author": "yusen",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/yscoder/vuepress-theme-indigo/issues"
29 | },
30 | "homepage": "https://github.com/yscoder/vuepress-theme-indigo#readme",
31 | "dependencies": {
32 | "@fortawesome/fontawesome-free-webfonts": "^1.0.6",
33 | "autoprefixer": "^8.2.0",
34 | "buble": "^0.19.3",
35 | "buble-loader": "^0.5.0",
36 | "chalk": "^2.3.2",
37 | "chokidar": "^2.0.3",
38 | "commander": "^2.15.1",
39 | "connect-history-api-fallback": "^1.5.0",
40 | "copy-webpack-plugin": "^4.5.1",
41 | "css-loader": "^0.28.11",
42 | "dayjs": "^1.5.16",
43 | "diacritics": "^1.3.0",
44 | "docsearch.js": "^2.5.2",
45 | "es6-promise": "^4.2.4",
46 | "escape-html": "^1.0.3",
47 | "file-loader": "^1.1.11",
48 | "fs-extra": "^5.0.0",
49 | "globby": "^8.0.1",
50 | "gray-matter": "^4.0.1",
51 | "js-yaml": "^3.11.0",
52 | "koa-connect": "^2.0.1",
53 | "koa-mount": "^3.0.0",
54 | "koa-static": "^4.0.2",
55 | "loader-utils": "^1.1.0",
56 | "lodash": "^4.17.10",
57 | "lru-cache": "^4.1.2",
58 | "markdown-it": "^8.4.1",
59 | "markdown-it-anchor": "^4.0.0",
60 | "markdown-it-container": "^2.0.0",
61 | "markdown-it-emoji": "^1.4.0",
62 | "markdown-it-table-of-contents": "^0.3.3",
63 | "mini-css-extract-plugin": "^0.4.0",
64 | "nprogress": "^0.2.0",
65 | "object-assign": "^4.1.1",
66 | "optimize-css-assets-webpack-plugin": "^4.0.0",
67 | "portfinder": "^1.0.13",
68 | "postcss-loader": "^2.1.3",
69 | "prismjs": "^1.13.0",
70 | "register-service-worker": "^1.2.0",
71 | "semver": "^5.5.0",
72 | "stylus": "^0.54.5",
73 | "stylus-loader": "^3.0.2",
74 | "toml": "^2.3.3",
75 | "url-loader": "^1.0.1",
76 | "vue": "^2.5.16",
77 | "vue-loader": "^15.0.0-rc.1",
78 | "vue-router": "^3.0.1",
79 | "vue-server-renderer": "^2.5.16",
80 | "vue-template-compiler": "^2.5.16",
81 | "vuepress-html-webpack-plugin": "^3.2.0",
82 | "vuetify": "^1.0.17",
83 | "webpack": "^4.5.0",
84 | "webpack-chain": "^4.6.0",
85 | "webpack-merge": "^4.1.2",
86 | "webpack-node-externals": "^1.7.2",
87 | "webpack-serve": "^0.3.1",
88 | "webpackbar": "^2.6.1",
89 | "workbox-build": "^3.1.0"
90 | },
91 | "devDependencies": {
92 | "conventional-changelog": "^1.1.23",
93 | "eslint": "^4.19.1",
94 | "eslint-plugin-vue-libs": "^2.1.0",
95 | "lint-staged": "^7.0.4",
96 | "vuepress-theme-vue": "^1.0.0",
97 | "yorkie": "^1.0.3"
98 | },
99 | "engines": {
100 | "node": ">=8"
101 | },
102 | "browserslist": [
103 | ">1%"
104 | ]
105 | }
106 |
--------------------------------------------------------------------------------
/src/posts/webpack-use-lodash.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Webpack按需打包Lodash的几种方式
3 | tags: [Webpack, Lodash]
4 | date: 2016-12-17 01:41:21
5 | description: Webpakck打包优化,按需打包Lodash模块。
6 | ---
7 |
8 | 在数据操作时,Lodash 就是我的弹药库,不管遇到多复杂的数据结构都能用一些函数轻松拆解。
9 |
10 | ES6 中也新增了诸多新的对象函数,一些简单的项目中 ES6 就足够使用了,但还是会有例外的情况引用了少数的 Lodash 函数。一个完整的 Lodash 库,即使是压缩后,现最新版本也有 `71k` 的体积。不能为了吃一口饭而买下一个饭店啊。
11 |
12 | 针对这个问题,其实已经有很多可选方案了。
13 |
14 |
15 |
16 | ## 函数模块
17 |
18 | Lodash 中的每个函数在 NPM 都有一个单独的发布模块。[NPM: results for ‘lodash’](https://www.npmjs.com/search?q=lodash)
19 | 假如你只需要使用`_.isEqual`,那么你只需要安装`lodash.isequal`模块,然后按以下方式引用。
20 |
21 | ```js
22 | var isEqual = require('lodash.isequal')
23 | // or ES6
24 | import isEqual from 'lodash.isequal'
25 |
26 | isEqual([1, 2, 3], [1, 2, 3]) // true
27 | ```
28 |
29 | ## 全路径引用
30 |
31 | 在你完整安装 Lodash 后,可以按`lodash/函数名`的格式单独引入需要的函数模块。
32 |
33 | ```js
34 | var difference = require('lodash/difference')
35 | // or ES6
36 | import difference from 'lodash/difference'
37 |
38 | difference([1, 2], [1, 3]) // [2]
39 | ```
40 |
41 | ## 使用插件优化
42 |
43 | 在简单场景下,以上两种方式足以解决问题。而遇到复杂的数据对象时,我们不得不在一个文件中引入多个 Lodash 函数,这样就需要在文件中写多个`require`或`import`相关函数。
44 |
45 | ```js
46 | import remove from 'lodash/remove'
47 | import uniq from 'lodash/uniq'
48 | import invokeMap from 'lodash/invokeMap'
49 | import sortBy from 'lodash/sortBy'
50 | // more...
51 | ```
52 |
53 | 正写到关键处却因为引入一个函数要拉到文件头部去定义引用而打乱了思路,很不爽!
54 |
55 | 于是我机智的到 Github 去搜索了`webpack`和`lodash`两个关键词的组合,排在首位的 [lodash-webpack-plugin](https://github.com/lodash/lodash-webpack-plugin) 就是为了解决这个问题而生。
56 |
57 | 使用时需要以下模块,其实除了前两个剩下的一般都已安装了:
58 |
59 | ```bash
60 | $ npm i -S lodash-webpack-plugin babel-plugin-lodash babel-core babel-loader babel-preset-es2015 webpack
61 | ```
62 |
63 | **配置:**
64 |
65 | ```js webpack.config.js
66 | var LodashModuleReplacementPlugin = require('lodash-webpack-plugin')
67 | var webpack = require('webpack')
68 |
69 | module.exports = {
70 | module: {
71 | loaders: [
72 | {
73 | loader: 'babel',
74 | test: /\.js$/,
75 | exclude: /node_modules/,
76 | query: {
77 | plugins: ['transform-runtime', 'lodash'],
78 | presets: ['es2015']
79 | }
80 | }
81 | ]
82 | },
83 | plugins: [new LodashModuleReplacementPlugin(), new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin()]
84 | }
85 | ```
86 |
87 | 其中`babel-plugin-lodash`的配置,也就是`plugins: ['lodash']`,并不是一定要在`loaders`中,也可以单独定义`babel`。
88 |
89 | ```js webpack.config.js
90 | var LodashModuleReplacementPlugin = require('lodash-webpack-plugin')
91 | var webpack = require('webpack')
92 |
93 | module.exports = {
94 | module: {
95 | loaders: [
96 | {
97 | loader: 'babel',
98 | test: /\.js$/,
99 | exclude: /node_modules/
100 | }
101 | ]
102 | },
103 | babel: {
104 | presets: ['es2015'],
105 | plugins: ['transform-runtime', 'lodash']
106 | },
107 | plugins: [new LodashModuleReplacementPlugin(), new webpack.optimize.OccurrenceOrderPlugin(), new webpack.optimize.UglifyJsPlugin()]
108 | }
109 | ```
110 |
111 | 又或者是`.babelrc`文件中。
112 |
113 | 以上工作完成了,在每个你需要使用 lodash 函数的文件中只需要引用一次 lodash,即可调用任意函数而不会造成完全打包。
114 |
115 | ```js
116 | import _ from 'lodash'
117 |
118 | _.add(1, 2) // 打包时只会引入这一个函数模块
119 | ```
120 |
121 | > 注意:必须要使用 ES2015 的模块引用方式才有效。
122 |
123 | ## End
124 |
125 | 以上即是我目前所知道的几种方式,如果哪位朋友有更好的方式(比如只需要全局引入一次),请一定分享与我!😋
126 |
--------------------------------------------------------------------------------
/bin/vuepress.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const chalk = require('chalk')
4 | const semver = require('semver')
5 | const requiredVersion = require('../package.json').engines.node
6 |
7 | if (!semver.satisfies(process.version, requiredVersion)) {
8 | console.log(chalk.red(
9 | `\n[vuepress] minimum Node version not met:` +
10 | `\nYou are using Node ${process.version}, but VuePress ` +
11 | `requires Node ${requiredVersion}.\nPlease upgrade your Node version.\n`
12 | ))
13 | process.exit(1)
14 | }
15 |
16 | const path = require('path')
17 | const { dev, build, eject } = require('../lib')
18 |
19 | const program = require('commander')
20 |
21 | program
22 | .version(require('../package.json').version)
23 | .usage(' [options]')
24 |
25 | program
26 | .command('dev [targetDir]')
27 | .description('start development server')
28 | .option('-p, --port ', 'use specified port (default: 8080)')
29 | .option('-h, --host ', 'use specified host (default: 0.0.0.0)')
30 | .action((dir = '.', { host, port }) => {
31 | wrapCommand(dev)(path.resolve(dir), { host, port })
32 | })
33 |
34 | program
35 | .command('build [targetDir]')
36 | .description('build dir as static site')
37 | .option('-d, --dest ', 'specify build output dir (default: .vuepress/dist)')
38 | .option('--debug', 'build in development mode for debugging')
39 | .action((dir = '.', { debug, dest }) => {
40 | const outDir = dest ? path.resolve(dest) : null
41 | wrapCommand(build)(path.resolve(dir), { debug, outDir })
42 | })
43 |
44 | program
45 | .command('eject [targetDir]')
46 | .description('copy the default theme into .vuepress/theme for customization.')
47 | .action((dir = '.') => {
48 | wrapCommand(eject)(path.resolve(dir))
49 | })
50 |
51 | // output help information on unknown commands
52 | program
53 | .arguments('')
54 | .action((cmd) => {
55 | program.outputHelp()
56 | console.log(` ` + chalk.red(`Unknown command ${chalk.yellow(cmd)}.`))
57 | console.log()
58 | })
59 |
60 | // add some useful info on help
61 | program.on('--help', () => {
62 | console.log()
63 | console.log(` Run ${chalk.cyan(`vuepress --help`)} for detailed usage of given command.`)
64 | console.log()
65 | })
66 |
67 | program.commands.forEach(c => c.on('--help', () => console.log()))
68 |
69 | // enhance common error messages
70 | const enhanceErrorMessages = (methodName, log) => {
71 | program.Command.prototype[methodName] = function (...args) {
72 | if (methodName === 'unknownOption' && this._allowUnknownOption) {
73 | return
74 | }
75 | this.outputHelp()
76 | console.log(` ` + chalk.red(log(...args)))
77 | console.log()
78 | process.exit(1)
79 | }
80 | }
81 |
82 | enhanceErrorMessages('missingArgument', argName => {
83 | return `Missing required argument ${chalk.yellow(`<${argName}>`)}.`
84 | })
85 |
86 | enhanceErrorMessages('unknownOption', optionName => {
87 | return `Unknown option ${chalk.yellow(optionName)}.`
88 | })
89 |
90 | enhanceErrorMessages('optionMissingArgument', (option, flag) => {
91 | return `Missing required argument for option ${chalk.yellow(option.flags)}` + (
92 | flag ? `, got ${chalk.yellow(flag)}` : ``
93 | )
94 | })
95 |
96 | program.parse(process.argv)
97 |
98 | if (!process.argv.slice(2).length) {
99 | program.outputHelp()
100 | }
101 |
102 | function wrapCommand (fn) {
103 | return (...args) => {
104 | return fn(...args).catch(err => {
105 | console.error(chalk.red(err.stack))
106 | })
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/lib/webpack/markdownLoader.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const hash = require('hash-sum')
4 | const { EventEmitter } = require('events')
5 | const { getOptions } = require('loader-utils')
6 | const { inferTitle, extractHeaders, parseFrontmatter } = require('../util')
7 | const LRU = require('lru-cache')
8 |
9 | const cache = LRU({ max: 1000 })
10 | const devCache = LRU({ max: 1000 })
11 |
12 | module.exports = function (src) {
13 | const isProd = process.env.NODE_ENV === 'production'
14 | const isServer = this.target === 'node'
15 | const { markdown, sourceDir } = getOptions(this)
16 |
17 | // we implement a manual cache here because this loader is chained before
18 | // vue-loader, and will be applied on the same file multiple times when
19 | // selecting the individual blocks.
20 | const file = this.resourcePath
21 | const key = hash(file + src)
22 | const cached = cache.get(key)
23 | if (cached && (isProd || /\?vue/.test(this.resourceQuery))) {
24 | return cached
25 | }
26 |
27 | const frontmatter = parseFrontmatter(src)
28 | const content = frontmatter.content
29 |
30 | if (!isProd && !isServer) {
31 | const inferredTitle = inferTitle(frontmatter)
32 | const headers = extractHeaders(content, ['h2', 'h3'], markdown)
33 | delete frontmatter.content
34 |
35 | // diff frontmatter and title, since they are not going to be part of the
36 | // returned component, changes in frontmatter do not trigger proper updates
37 | const cachedData = devCache.get(file)
38 | if (cachedData && (
39 | cachedData.inferredTitle !== inferredTitle ||
40 | JSON.stringify(cachedData.frontmatter) !== JSON.stringify(frontmatter) ||
41 | headersChanged(cachedData.headers, headers)
42 | )) {
43 | // frontmatter changed... need to do a full reload
44 | module.exports.frontmatterEmitter.emit('update')
45 | }
46 |
47 | devCache.set(file, {
48 | headers,
49 | frontmatter,
50 | inferredTitle
51 | })
52 | }
53 |
54 | // the render method has been augmented to allow plugins to
55 | // register data during render
56 | const { html, data: { hoistedTags, links }} = markdown.render(content)
57 |
58 | // check if relative links are valid
59 | links && links.forEach(link => {
60 | const shortname = link
61 | .replace(/#[\w-]*$/, '')
62 | .replace(/\.html$/, '.md')
63 | const filename = shortname
64 | .replace(/\/$/, '/README.md')
65 | .replace(/^\//, sourceDir + '/')
66 | const altname = shortname
67 | .replace(/\/$/, '/index.md')
68 | .replace(/^\//, sourceDir + '/')
69 | const dir = path.dirname(this.resourcePath)
70 | const file = path.resolve(dir, filename)
71 | const altfile = altname !== filename ? path.resolve(dir, altname) : null
72 | if (!fs.existsSync(file) && (altfile && !fs.existsSync(altfile))) {
73 | this.emitWarning(
74 | new Error(
75 | `\nFile for relative link "${link}" does not exist.\n` +
76 | `(Resolved file: ${file})\n`
77 | )
78 | )
79 | }
80 | })
81 |
82 | const res = (
83 | `\n` +
84 | `${html}\n` +
85 | `\n` +
86 | (hoistedTags || []).join('\n')
87 | )
88 | cache.set(key, res)
89 | return res
90 | }
91 |
92 | function headersChanged (a, b) {
93 | if (a.length !== b.length) return true
94 | return a.some((h, i) => (
95 | h.title !== b[i].title ||
96 | h.level !== b[i].level
97 | ))
98 | }
99 |
100 | module.exports.frontmatterEmitter = new EventEmitter()
101 |
--------------------------------------------------------------------------------
/src/.vuepress/config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const nodeExternals = require('webpack-node-externals')
3 |
4 | const resolve = pathName => path.join(__dirname, pathName)
5 | const isProd = process.env.NODE_ENV === 'production'
6 |
7 | module.exports = {
8 | base: isProd ? '/vuepress-theme-indigo/' : '/',
9 | dest: 'docs',
10 | title: "Yusen's Blog",
11 | description: '王昱森的博客。无所谓做什么,只要是当前最感兴趣的事!随心、随性、随缘!',
12 | head: [
13 | ['link', { rel: 'shortcut icon', href: '/favicon.ico' }],
14 | ['link', { rel: 'manifest', href: '/manifest.json' }],
15 | ['meta', { name: 'theme-color', content: '#3F51B5' }],
16 | ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
17 | ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }],
18 | ['link', { rel: 'apple-touch-icon', href: '/icons/192.png' }],
19 | // ['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }],
20 | ['meta', { name: 'msapplication-TileImage', content: '/icons/192.png' }],
21 | ['meta', { name: 'msapplication-TileColor', content: '#3F51B5' }]
22 | ],
23 | serviceWorker: true,
24 | theme: '',
25 | locales: {
26 | '/': {
27 | lang: 'zh-CN',
28 | title: "Yusen's Blog",
29 | description: '王昱森的博客。无所谓做什么,只要是当前最感兴趣的事!随心、随性、随缘!'
30 | }
31 | },
32 | configureWebpack: (config, isServer) => {
33 | const myConfig = {
34 | resolve: {
35 | alias: {
36 | '@pub': resolve('./public')
37 | }
38 | },
39 | module: {
40 | rules: [{
41 | test: /vuetify.+\.js$/,
42 | loader: resolve('./ignoreStylus'),
43 | }]
44 | }
45 | }
46 | if (isServer) {
47 | myConfig.externals = nodeExternals({
48 | whitelist: [/vuetify/, /fortawesome/, /prismjs/]
49 | })
50 | }
51 | return myConfig
52 | },
53 | themeConfig: {
54 | lang: 'zh-CN',
55 | postDir: '/posts',
56 | subTitle: '学习弯道超车的技巧!',
57 | author: '王昱森',
58 | email: 'iyusen@foxmail.com',
59 | since: 2015,
60 | avatar: '/face.jpg',
61 | avatarLink: '/',
62 | menus: [
63 | // icons by https://fontawesome.com/icons
64 | {
65 | text: 'Home',
66 | icon: 'fa fa-home',
67 | url: '/'
68 | },
69 | {
70 | text: 'Tags',
71 | icon: 'fa fa-tag',
72 | url: '/tags'
73 | },
74 | {
75 | text: 'Github',
76 | icon: 'fab fa-github',
77 | url: 'https://github.com/yscoder',
78 | external: true
79 | },
80 | {
81 | text: 'Weibo',
82 | icon: 'fab fa-weibo',
83 | url: 'https://www.weibo.com/ysweb',
84 | external: true
85 | },
86 | {
87 | text: 'About',
88 | icon: 'fa fa-user-secret',
89 | url: '/about'
90 | }
91 | ],
92 | socials: ['Weibo', 'QQ', 'Facebook', 'Twitter', 'GooglePlus'],
93 | colors: {
94 | // generate by https://vuetifyjs.com/zh-Hans/theme-generator
95 | primary: '#3F51B5',
96 | secondary: '#6d6d6d',
97 | accent: '#E91E63',
98 | error: '#f44336',
99 | warning: '#FFC107',
100 | info: '#00B8D4',
101 | success: '#4caf50'
102 | },
103 | format: {
104 | date: 'YYYY年MM月DD日',
105 | dateTime: 'YYYY年MM月DD日 HH:mm:ss'
106 | },
107 | pagination: {
108 | path: '/page/:pageNum',
109 | pageSize: 5
110 | },
111 | tags: {
112 | path: '/tags/:tagName'
113 | },
114 | categories: {
115 | path: '/categories/:category'
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/lib/default-theme/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ data.heroText || $title || 'Hello' }}
6 |
7 | {{ data.tagline || $description || 'Welcome to your VuePress site' }}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{ feature.title }}
16 | {{ feature.details }}
17 |
18 |
19 |
20 |
23 |
24 |
25 |
26 |
44 |
45 |
135 |
--------------------------------------------------------------------------------
/src/posts/write-good-front-end-component.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 编写良好的前端组件
3 | tags: [前端, 组件, Vue, React]
4 | date: 2017-03-17 10:22:38
5 | description: 编写良好的前端组件
6 | ---
7 | Vue 和 React 的大红大火,带来的是组件化和数据驱动的开发方式。Demo 很美好,但如果没有一定的实际开发经验积累,总是能把一个功能模块写成浆糊。
8 | 依托于 Webpack 等构建工具,使得前端代码具备了后端编程语言的代码组织能力,摆脱了传统的「一泻而下」式的代码编写。至此,作为前端也该对自己的代码有更高的要求。
9 |
10 |
11 | ## 组件职责划分
12 |
13 | > 一个组件只做一件事,基于功能做好职责划分。
14 |
15 | ### 无状态组件
16 |
17 | 公司用的是 Vue,最近又接触了下 React。
18 | 对比来说,React 由于 jsx 式(js和html混合)的写法,加上构建工具的模块化管理,一个文件中可以有多个组件。还支持纯函数式的**无状态组件**,只是单纯的接受数据渲染 DOM,没有生命周期等额外的概念。
19 |
20 | 
21 |
22 | ```js React
23 | // 无状态组件
24 | const noStatus = props => {props.title}
25 | ```
26 |
27 | 看起来就像一个简单的模版渲染过程。
28 |
29 | Vue 中没有**无状态组件**的概念,但实际上也存在类似功能的组件形式。比如图标组件,只接收 `props` 渲染模版,不做多余的动作。
30 |
31 | ```html Vue
32 |
33 |
34 |
35 |
42 | ```
43 |
44 | ### 端对端组件
45 |
46 | 端对端组件指的是不需要依赖外部给予,自身就可以负责从数据获取到展示过程的组件。
47 | 这类组件在业务开发中也很常见,比如公共的分类选择器。由于到多处调用,如果每次用的时候都由外部请求数据在调用组件展示,那么这个请求数据的代码显然是个重复的逻辑,索性直接就写入到组件内部了。
48 |
49 | 
50 |
51 | > 当然端对端组件也有缺陷。就是每次调用不管数据有没有变化,都会重新请求,造成冗余。如何改善,那又是另一个话题了。这篇文章中有提到:[徐飞:复杂单页应用的数据层设计](https://github.com/xufei/blog/issues)
52 |
53 | ### UI组件
54 |
55 | UI 组件指的是界面扩展类组件,比如:输入框、表格、树、下拉框等。像 Element、Vux 等组件库均属于此类组件。
56 |
57 | 
58 |
59 | 此类组件的特点是:复用性强,只通过 `props`、`events` 和 `slots` 等组件接口与外部通信。
60 | 更像是一个对 HTML 的扩展标签。
61 |
62 | ### 业务组件
63 |
64 | 业务组件通常是根据最小业务状态抽象而出,有些业务组件也具有一定的复用性,但大多数是一次性组件。
65 |
66 | 
67 |
68 | 之前提到的组件数据或自给自足(端对端组件),或来自 `props`,那么业务组件的数据呢?
69 |
70 | 1. props
71 | 2. global state
72 |
73 | 只能是以上两种了,如果还是组件内部去请求数据,那么就还是属于端对端组件了。
74 |
75 | ### 容器组件
76 |
77 | 这类组件就是一个盒子,一般当作一个业务子模块的入口,比如一个路由指向的组件。
78 |
79 | 
80 |
81 | 通常是这种形式:
82 |
83 | ```html
84 |
85 |
86 |
87 |
88 |
89 | ```
90 |
91 | * 容器组件内的子组件通常具有业务或数据依赖关系。
92 | * 如果没有使用全局状态管理,那么容器组件就是负责通过 `props` 分发数据到各个子组件,在通过 `events` 处理各个子组件的业务响应。此时容器组件需要做数据请求工作。
93 | * 如果使用了全局状态管理,那么容器内部的业务组件可以自行调用全局状态处理业务。但并不是说此时容器组件什么都不用干了。即使不需要请求数据,还是有许多组件间或一个业务模块内的诸多统筹工作要做。
94 |
95 | 把上面的各类组件组装到一起就组成一个业务模块。
96 |
97 | 
98 |
99 | ## 组件设计原则
100 |
101 | ### 尽可能的减少状态
102 |
103 | 1. 如果一个数据可以由另一个 state 变换得到,那么这个数据就不是一个 state。只需要写一个变换的处理函数,在 Vue 中可以使用计算属性。
104 | 2. 如果你的 state 是一个数组,而模版最外层是渲染这个数组,那么你需要做的事是把渲染的项作为一个组件,只接受一个单级对象形式的数据,由外部决定这个组件的展示次数。
105 | 3. 如果一个数据是固定的,不会变化的常量,那么这个数据就如同 HTML 固定的站点标题一样,写死或作为全局配置属性等,不属于 state。
106 | 4. 如果一个数据需要从外部得到,它应该属于 props。
107 | 5. 如果组件和兄弟组件拥有相同的 state,那么这个 state 应该放到更高的层级中,使用 props 传递到两个组件中。
108 |
109 | ### 合理的依赖关系
110 |
111 | 1. 父组件不依赖子组件。要做到当我们把子组件删除后,只是丢失了一个功能,或一个模块等,而不会造成父组件及兄弟组件功能异常。
112 | 2. 子组件基于父组件传递 props 作出个性化展示。
113 |
114 | ### 扁平化参数
115 |
116 | 像 HTML 原生元素那样,只接受原始类型(字符串、数值、布尔值和函数)作为属性,避免复杂的对象。当然,数据除外。
117 |
118 | ```html
119 |
120 |
125 |
126 |
127 |
128 |
129 | ```
130 |
131 | ### 良好的接口设计
132 |
133 | 1. 把组件内部可以完成的工作做到极致。虽然提倡拥抱变化,但接口不是越多越好。
134 | 2. 如果常量变为 props 能应对更多的场景,那么就可以作为 props。原有的常量可作为默认值。
135 | 3. 如果组件不能提供调用者所需求的功能,那么这个组件的接口还不够完善。
136 | 4. 如果需要为了某一调用者编写大量特定需求的代码,那么可以考虑通过扩展等方式构建一个新的组件。
137 | 5. 保证组件的属性和事件足够的给大多数的组件使用。
138 |
139 | ## End
140 |
141 | 设计模式六大原则在组件设计中也有适用的地方。
142 |
--------------------------------------------------------------------------------
/lib/default-theme/Page.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ editLinkText }}
6 |
7 |
8 |
9 |
10 |
11 | ←
12 | {{ prev.title || prev.path }}
13 |
14 |
15 |
16 |
17 | {{ next.title || next.path }}
18 | →
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
117 |
118 |
141 |
--------------------------------------------------------------------------------
/lib/default-theme/styles/theme.styl:
--------------------------------------------------------------------------------
1 | @require './config'
2 | @require './nprogress'
3 | @require './code'
4 | @require './custom-blocks'
5 | @require './arrow'
6 |
7 | html, body
8 | padding 0
9 | margin 0
10 |
11 | body
12 | font-family -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif
13 | -webkit-font-smoothing antialiased
14 | -moz-osx-font-smoothing grayscale
15 | font-size 16px
16 | color $textColor
17 |
18 | .page
19 | padding-left $sidebarWidth
20 |
21 | .navbar
22 | position fixed
23 | z-index 20
24 | top 0
25 | left 0
26 | right 0
27 | height $navbarHeight
28 | background-color #fff
29 | box-sizing border-box
30 | border-bottom 1px solid $borderColor
31 |
32 | .sidebar-mask
33 | position fixed
34 | z-index 9
35 | top 0
36 | left 0
37 | width 100vw
38 | height 100vh
39 | display none
40 |
41 | .sidebar
42 | font-size 15px
43 | background-color #fff
44 | width $sidebarWidth
45 | position fixed
46 | z-index 10
47 | margin 0
48 | top $navbarHeight
49 | left 0
50 | bottom 0
51 | box-sizing border-box
52 | border-right 1px solid $borderColor
53 | overflow-y auto
54 |
55 | .content:not(.custom)
56 | max-width $contentWidth
57 | margin 0 auto
58 | padding 2rem 2.5rem
59 | > *:first-child
60 | margin-top $navbarHeight
61 | a:hover
62 | text-decoration underline
63 | p.demo
64 | padding 1rem 1.5rem
65 | border 1px solid #ddd
66 | border-radius 4px
67 | img
68 | max-width 100%
69 |
70 | .content.custom
71 | padding 0
72 | margin 0
73 |
74 | a
75 | font-weight 500
76 | color $accentColor
77 | text-decoration none
78 |
79 | p a code
80 | font-weight 400
81 | color $accentColor
82 |
83 | kbd
84 | background #eee
85 | border solid 0.15rem #ddd
86 | border-bottom solid 0.25rem #ddd
87 | border-radius 0.15rem
88 | padding 0 0.15em
89 |
90 | blockquote
91 | font-size 1.2rem
92 | color #999
93 | border-left .25rem solid #dfe2e5
94 | margin-left 0
95 | padding-left 1rem
96 |
97 | ul, ol
98 | padding-left 1.2em
99 |
100 | strong
101 | font-weight 600
102 |
103 | h1, h2, h3, h4, h5, h6
104 | font-weight 600
105 | line-height 1.25
106 | .content:not(.custom) > &
107 | margin-top (0.5rem - $navbarHeight)
108 | padding-top ($navbarHeight + 1rem)
109 | margin-bottom 0
110 | &:first-child
111 | margin-top -1.5rem
112 | margin-bottom 1rem
113 | + p, + pre, + .custom-block
114 | margin-top 2rem
115 | &:hover .header-anchor
116 | opacity: 1
117 |
118 | h1
119 | font-size 2.2rem
120 |
121 | h2
122 | font-size 1.65rem
123 | padding-bottom .3rem
124 | border-bottom 1px solid $borderColor
125 |
126 | h3
127 | font-size 1.35rem
128 |
129 | a.header-anchor
130 | font-size 0.85em
131 | float left
132 | margin-left -0.87em
133 | padding-right 0.23em
134 | margin-top 0.125em
135 | opacity 0
136 | &:hover
137 | text-decoration none
138 |
139 | code, kbd
140 | font-family source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace
141 |
142 | p, ul, ol
143 | line-height 1.7
144 |
145 | hr
146 | border 0
147 | border-top 1px solid $borderColor
148 |
149 | table
150 | border-collapse collapse
151 | margin 1rem 0
152 |
153 | tr
154 | border-top 1px solid #dfe2e5
155 | &:nth-child(2n)
156 | background-color #f6f8fa
157 |
158 | th, td
159 | border 1px solid #dfe2e5
160 | padding .6em 1em
161 |
162 | .custom-layout
163 | padding-top $navbarHeight
164 |
165 | .theme-container
166 | &.sidebar-open
167 | .sidebar-mask
168 | display: block
169 | &.no-navbar
170 | .content:not(.custom)
171 | h1, h2, h3, h4, h5, h6
172 | margin-top 1.5rem
173 | padding-top 0
174 |
175 |
176 | @media (min-width: ($MQMobile + 1px))
177 | .theme-container.no-sidebar
178 | .sidebar
179 | display none
180 | .page
181 | padding-left 0
182 |
183 | @require './mobile.styl'
184 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/styles/content.styl:
--------------------------------------------------------------------------------
1 | code, kbd, pre, samp
2 | font-family: $font-code
3 |
4 | .content.custom {
5 | display: block;
6 | min-height: 150px;
7 | font-size: 16px;
8 | line-height: 1.8;
9 | color: $content-color;
10 |
11 | p {
12 | word-wrap: break-word;
13 | }
14 |
15 | h1, h2, h3, h4, h5, h6, ol, ul, pre, table, figure {
16 | margin-bottom: 1.2em
17 | }
18 |
19 | h1, h2, h3, h4, h5, h6 {
20 | padding-top: 3em;
21 | margin-top: -1.8em;
22 | text-transform: capitalize;
23 |
24 | .header-anchor {
25 | margin-left: -1em;
26 | padding-right: 4px;
27 | color: $light-primary-color;
28 | opacity: 0;
29 | text-decoration: none;
30 | }
31 |
32 | &:hover {
33 | .header-anchor {
34 | opacity: 1;
35 | }
36 | }
37 | }
38 |
39 | a {
40 | position relative
41 | text-decoration none
42 | &:after {
43 | content ''
44 | position absolute
45 | left 0
46 | bottom 0
47 | height 1px
48 | width 100%
49 | background rgba($primary-color, .2)
50 | transition height 200ms ease-in
51 | }
52 | &:hover:after {
53 | height 100%
54 | }
55 | }
56 |
57 | blockquote {
58 | margin 2em 0 2em 1em
59 | padding 4px 24px
60 | font-style italic
61 | color $gray-color
62 | border-left .375em solid $primary-color
63 | p {
64 | margin-top: 0
65 | &:last-child {
66 | margin-bottom: 0
67 | }
68 | }
69 | footer {
70 | font-size 80%
71 | }
72 | }
73 |
74 | strong {
75 | font-weight: 600;
76 | margin: 0 0.2em;
77 | }
78 |
79 | code, kbd {
80 | display: inline;
81 | margin: 0 0.3em;
82 | padding: .125em .25em;
83 | box-shadow: none;
84 | }
85 |
86 | pre,
87 | pre[class*="language-"] {
88 | margin-left: -1.75em;
89 | margin-right: -1.75em;
90 | padding: 1.25em 1.75em;
91 | white-space: pre-wrap;
92 | word-break: break-word;
93 | line-height: 1.6;
94 | background: #3e3e3e;
95 | }
96 | pre[class*='language-'] {
97 | position relative
98 | &:before {
99 | position: absolute;
100 | top: 0.8em;
101 | right: 1em;
102 | font-size: .86em;
103 | color: rgba(255, 255, 255, 0.7);
104 | }
105 | }
106 |
107 | pre {
108 | code {
109 | margin: 0;
110 | padding: 0;
111 | background-color: transparent;
112 | font-size: .86em;
113 | font-weight: 500;
114 | color: rgba(255, 255, 255, 0.8);
115 |
116 | &:before, &:after {
117 | content: '';
118 | letter-spacing: 0;
119 | }
120 | }
121 | }
122 |
123 | ol, ul {
124 | padding-left: 2em;
125 | }
126 |
127 | table {
128 | width: 100%;
129 | border: 1px solid #dedede;
130 | border-collapse: collapse;
131 | border-spacing: 0;
132 |
133 | thead {
134 | tr {
135 | background: #f8f8f8;
136 | }
137 | }
138 |
139 | tbody {
140 | tr {
141 | &:hover {
142 | background: #efefef;
143 | }
144 | }
145 | }
146 |
147 | td, th {
148 | border: 1px solid #dedede;
149 | padding: .375em .75em;
150 | }
151 | }
152 |
153 | video, audio {
154 | max-width: 100%;
155 | }
156 |
157 | .highlighted-line {
158 | display: block;
159 | margin: 0 -1.5rem;
160 | padding: 0 1.5rem;
161 | background-color: rgba(0, 0, 0, 66%);
162 | }
163 |
164 | *:first-child {
165 | margin-top 0
166 | }
167 | }
168 |
169 | for type, exts in $code-languages
170 | pre
171 | &.language-{exts[0]}:before
172 | content type
173 | if exts[1]
174 | &.language-{exts[1]}:before
175 | content type
176 |
177 | .custom-block
178 | margin-bottom 1.2em
179 | padding 1em 1.5em 0.2em
180 | border-left-width .375em
181 | border-left-style solid
182 | border-radius 4px
183 | &-title
184 | font-weight 600
185 | for type, color in $custom-blocks
186 | &.{type}
187 | color darken(color, 20%)
188 | border-color color!important
189 | background lighten(color, 90%)!important
190 |
--------------------------------------------------------------------------------
/lib/default-theme/AlgoliaSearchBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
35 |
36 |
132 |
--------------------------------------------------------------------------------
/lib/default-theme/NavLinks.vue:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
100 |
101 |
134 |
--------------------------------------------------------------------------------
/src/.vuepress/theme/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
14 |
15 |
16 |
18 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
34 |
35 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/lib/default-theme/DropdownLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ item.text }}
5 |
6 |
7 |
8 |
25 |
26 |
27 |
28 |
29 |
53 |
54 |
158 |
--------------------------------------------------------------------------------
/lib/dev.js:
--------------------------------------------------------------------------------
1 | module.exports = async function dev (sourceDir, cliOptions = {}) {
2 | const fs = require('fs')
3 | const path = require('path')
4 | const chalk = require('chalk')
5 | const webpack = require('webpack')
6 | const chokidar = require('chokidar')
7 | const serve = require('webpack-serve')
8 | const convert = require('koa-connect')
9 | const mount = require('koa-mount')
10 | const serveStatic = require('koa-static')
11 | const history = require('connect-history-api-fallback')
12 | const portfinder = require('portfinder')
13 |
14 | const prepare = require('./prepare')
15 | const HeadPlugin = require('./webpack/HeadPlugin')
16 | const createClientConfig = require('./webpack/createClientConfig')
17 | const { applyUserWebpackConfig } = require('./util')
18 | const { frontmatterEmitter } = require('./webpack/markdownLoader')
19 |
20 | process.stdout.write('Extracting site metadata...')
21 | const options = await prepare(sourceDir)
22 |
23 | // setup watchers to update options and dynamically generated files
24 | const update = () => {
25 | prepare(sourceDir).catch(err => {
26 | console.error(chalk.red(err.stack))
27 | })
28 | }
29 |
30 | // watch add/remove of files
31 | const pagesWatcher = chokidar.watch([
32 | path.join(sourceDir, '**/*.md'),
33 | path.join(sourceDir, '.vuepress/components/**/*.vue')
34 | ], {
35 | ignored: '.vuepress/**/*.md',
36 | ignoreInitial: true
37 | })
38 | pagesWatcher.on('add', update)
39 | pagesWatcher.on('unlink', update)
40 | pagesWatcher.on('addDir', update)
41 | pagesWatcher.on('unlinkDir', update)
42 |
43 | // watch config file
44 | const configWatcher = chokidar.watch([
45 | path.join(sourceDir, '.vuepress/config.js')
46 | ], { ignoreInitial: true })
47 | configWatcher.on('change', update)
48 |
49 | // also listen for frontmatter changes from markdown files
50 | frontmatterEmitter.on('update', update)
51 |
52 | // resolve webpack config
53 | let config = createClientConfig(options, cliOptions)
54 |
55 | config
56 | .plugin('html')
57 | // using a fork of html-webpack-plugin to avoid it requiring webpack
58 | // internals from an incompatible version.
59 | .use(require('vuepress-html-webpack-plugin'), [{
60 | template: path.resolve(__dirname, 'app/index.dev.html')
61 | }])
62 |
63 | config
64 | .plugin('site-data')
65 | .use(HeadPlugin, [{
66 | tags: options.siteConfig.head || []
67 | }])
68 |
69 | config = config.toConfig()
70 | const userConfig = options.siteConfig.configureWebpack
71 | if (userConfig) {
72 | config = applyUserWebpackConfig(userConfig, config, false /* isServer */)
73 | }
74 |
75 | const compiler = webpack(config)
76 | // webpack-serve hot updates doesn't work properly over 0.0.0.0 on Windows,
77 | // but localhost does not allow visiting over network :/
78 | const defaultHost = process.platform === 'win32' ? 'localhost' : '0.0.0.0'
79 | const host = cliOptions.host || options.siteConfig.host || defaultHost
80 | const displayHost = host === defaultHost && process.platform !== 'win32'
81 | ? 'localhost'
82 | : host
83 | portfinder.basePort = cliOptions.port || options.siteConfig.port || 8080
84 | const port = await portfinder.getPortPromise()
85 |
86 | let isFirst = true
87 | compiler.hooks.done.tap('vuepress', () => {
88 | if (isFirst) {
89 | isFirst = false
90 | console.log(
91 | `\n VuePress dev server listening at ${
92 | chalk.cyan(`http://${displayHost}:${port}${options.publicPath}`)
93 | }\n`
94 | )
95 | } else {
96 | const time = new Date().toTimeString().match(/^[\d:]+/)[0]
97 | console.log(` ${chalk.gray(`[${time}]`)} ${chalk.green('✔')} successfully compiled.`)
98 | }
99 | })
100 |
101 | const nonExistentDir = path.resolve(__dirname, 'non-existent')
102 | await serve({
103 | // avoid project cwd from being served. Otherwise if the user has index.html
104 | // in cwd it would break the server
105 | content: [nonExistentDir],
106 | compiler,
107 | host,
108 | dev: { logLevel: 'warn' },
109 | hot: { logLevel: 'error' },
110 | logLevel: 'error',
111 | port,
112 | add: app => {
113 | const userPublic = path.resolve(sourceDir, '.vuepress/public')
114 | // respect base when serving static files...
115 | if (fs.existsSync(userPublic)) {
116 | app.use(mount(options.publicPath, serveStatic(userPublic)))
117 | }
118 |
119 | app.use(convert(history({
120 | rewrites: [
121 | { from: /\.html$/, to: '/' }
122 | ]
123 | })))
124 | }
125 | })
126 | }
127 |
--------------------------------------------------------------------------------
/src/posts/cursor-offset-at-input.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: JavaScript 获取输入时的光标位置及场景问题
3 | date: 2016-11-25 10:02:01
4 | tags: [JavaScript, Range, Selection]
5 | description: JavaScript 如何获取输入时的光标位置,在光标处插入或替换内容
6 | ---
7 |
8 | ## 前言
9 |
10 | 在输入编辑的业务场景中,可能会需要在光标当前的位置或附近显示提示选项。比如社交评论中的`@user`功能,要确保提示的用户列表总是出现在`@`字符右下方,又或者是在自定义编辑器中 autocomplete 语法提示,都需要获取光标当前的位置作为参照点。
11 |
12 |
13 |
14 | ## 两种位置
15 |
16 | 对于 WEB 开发来讲,当我们提到某某元素的位置,通常是指这个元素相对于父级或文档的像素单位坐标。而对于输入框中光标,就有了额外的区分。
17 |
18 | ### 相对于内容
19 |
20 | 相对于内容,光标位于第几个字符之后,姑且称之为**字符位置**吧。
21 |
22 | ### 相对于 UI
23 |
24 | 相对于 UI,也就是跟普通页面元素一样的**像素位置**了。
25 |
26 | ## 插入或替换内容
27 |
28 | 在前言提到的场景中,也有在光标位置处插入内容的需求,比如对选取文字加粗`text => text`等。
29 |
30 | ### textarea
31 |
32 | `textarea`元素可以很容易获取到选择的一段文字的起止位置。如果当前没有选择文字,则两个位置值都为光标右侧字符的索引,从 0 开始。
33 |
34 | ```js
35 | // 开始位置
36 | textarea.selectionStart
37 | // 结束位置
38 | textarea.selectionEnd
39 | ```
40 |
41 | 对于加粗功能,有了起止位置,就能获取到选择的文字内容,然后对内容进行替换。由于`textarea`不能包含子元素,只有纯文本,所以基于`textarea`实现加粗只能像用 Markdown 标记语法实现。
42 |
43 | ```js
44 | var selectedText = textarea.value.substring(textarea.selectionStart, textarea.selectionEnd)
45 | textarea.setRangeText('**' + selectedText + '**')
46 | ```
47 |
48 | > `textarea.setRangeText(text: String)` 把选中的文字替换为其他内容。
49 |
50 | ### contenteditable
51 |
52 | 也可能我们会使用`contenteditable`属性把一个元素变为可编辑元素。而上面所用的属性和函数都是普通元素所没有的,所以要换一种姿势实现。
53 |
54 | 还是以加粗功能为例。
55 |
56 | ```js
57 | // 获取文档中选中区域
58 | var range = window.getSelection().getRangeAt(0)
59 | var strongNode = document.createElement('strong')
60 | // 选中区域文本
61 | strongNode.innerHTML = range.toString()
62 | // 删除选中区
63 | range.deleteContents()
64 | // 在光标处插入新节点
65 | range.insertNode(strongNode)
66 | ```
67 |
68 | 基于`contenteditable`的可编辑元素,其中的内容均为子元素,文本为`textNode`,加粗使用 HTML 元素,插入或替换是对元素的操作。
69 |
70 | 如果想使用操作内容的思路实现会比较麻烦,因为可以获取到的起止位置是基于子元素的。
71 |
72 | ```html
73 | hello你好world
74 | ```
75 |
76 | 假如选中的文字是`你好wor`,调用相关 API 的输出如下。
77 |
78 | ```js
79 | // 当前在文档中选择的文本,document 和 window 都有这个函数
80 | // var selection = document.getSelection()
81 | var selection = window.getSelection()
82 | selection.anchorNode // 你好
83 | selection.anchorOffset // 0
84 | selection.focusNode // orld
85 | selection.focusOffset // 2
86 |
87 | // 或者使用 Range
88 | var range = selection.getRangeAt(0)
89 | range.startContainer // 你好
90 | range.startOffset // 0
91 | range.endContainer // orld
92 | range.endOffset // 2
93 | ```
94 |
95 | 最终可以获取到起止元素以及选中区域在开始元素内容中的字符位置和在结束元素内容中的字符位置。其中的起止元素均为`textNode`类型,通过`parentNode`获取到包裹元素。
96 |
97 | ```js
98 | range.startContainer.parentNode // 你好
99 | range.endContainer.parentNode // ...
100 | ```
101 |
102 | > 需要注意的是通过`Selection`和`Rang`获取到起止位置是有方向之分的,从左向右选择和从右向左选择得到的值是正好相反的。
103 |
104 | ## 基于光标像素位置创建内容
105 |
106 | 这里就要开始用像素位置,同样分为两种实现来讲。
107 |
108 | ### contenteditable
109 |
110 | 可编辑元素获取光标像素位置就像`textarea`获取光标的字符位置一样简单。
111 |
112 | ```js
113 | var range = window.getSelection().getRangeAt(0)
114 | range.getBoundingClientRect() // { width, height, top, right, bottom, right }
115 | ```
116 |
117 | 这么具体的尺寸值,实现自动完成真是 So easy!
118 |
119 | ### textarea
120 |
121 | `textarea`其中的内容都是纯文本,在 DOM 中不存在相关的对象,对于像素位置就得另作他想了。
122 |
123 | #### 基于行高和字体大小计算
124 |
125 | ```js
126 | // 1.获取光标结束位置
127 | var end = textarea.selectionEnd
128 | // 2.通过匹配光标之前文本中的换行符计算所在行
129 | var row = textarea.value.substring(0, end).match(/\r\n|\r|\n/).length
130 | // 3.计算 top,行高 * 行数 + 上填充 + 边框宽度
131 | var top = lineHeight * (row + 1) + paddingTop + borderWidth
132 | // 4.获取光标左侧的文本
133 | var leftText = textarea.value.split(/\r\n|\r|\n/)[row]
134 | // 5.影响一段文字所占宽度的因素太多,除字体大小、中英文、符号、字符间距等,还有字体、浏览器、系统等客观因素
135 | // var left = ...
136 | ```
137 |
138 | 这个方案的思路是没问题的,但是考虑所有问题的成本太高。虽然可以创建测试元素去计算文本宽度,但这个方案本身是从严谨的角度出发的。与其混在一块,直接用取巧的办法更简单。
139 |
140 | > ~~这个方案的潜台词是:明明可以靠脸吃饭,却偏偏要靠才华!~~ 🙄
141 |
142 | #### 镜像元素
143 |
144 | 文本不支持定位?那我创建 DOM 好了。
145 |
146 | ```js
147 | // 光标位置
148 | var end = textarea.selectionEnd
149 | // 光标前的内容
150 | var beforeText = textarea.value.slice(0, end)
151 | // 光标后的内容
152 | var afterText = textarea.value.slice(end)
153 | // 对影响 UI 的特殊元素编码
154 | var escape = function(text) {
155 | return text.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, '
')
156 | }
157 | // 创建镜像内容,复制样式
158 | var mirror = '' + escape(beforeText) + '|' + escape(afterText) + ''
159 | // 添加到 textarea 同级,注意设置定位及 zIndex,使两个元素重合
160 | textarea.insertAdjacentHTML('afterend', mirror)
161 | // 通过镜像元素中的假光标占位元素获取像素位置
162 | var cursor = document.getElementById('cursor')
163 | cursor.getBoundingClientRect() // { width, height, top, right, bottom, right }
164 | ```
165 |
166 | ## End
167 |
168 | 最后悄悄说一句,以上内容不兼容低版本 IE,但是 IE 毕竟主场运行,有些 API 反而是其他浏览器所没有的。就上面提到的案例来说,低版本 IE 也有对应的 API 可用。真是不想在 IE 上去浪费精力了,索性不提。
169 |
--------------------------------------------------------------------------------
/lib/default-theme/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
175 |
176 |
177 |
178 |
--------------------------------------------------------------------------------
/lib/default-theme/SearchBox.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
18 | -
22 |
23 | {{ s.title || s.path }}
24 | > {{ s.header.title }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
134 |
135 |
209 |
--------------------------------------------------------------------------------
/lib/default-theme/util.js:
--------------------------------------------------------------------------------
1 | export const hashRE = /#.*$/
2 | export const extRE = /\.(md|html)$/
3 | export const endingSlashRE = /\/$/
4 | export const outboundRE = /^(https?:|mailto:)/
5 |
6 | export function normalize (path) {
7 | return path
8 | .replace(hashRE, '')
9 | .replace(extRE, '')
10 | }
11 |
12 | export function getHash (path) {
13 | const match = path.match(hashRE)
14 | if (match) {
15 | return match[0]
16 | }
17 | }
18 |
19 | export function isExternal (path) {
20 | return outboundRE.test(path)
21 | }
22 |
23 | export function isMailto (path) {
24 | return /^mailto:/.test(path)
25 | }
26 |
27 | export function ensureExt (path) {
28 | if (isExternal(path)) {
29 | return path
30 | }
31 | const hashMatch = path.match(hashRE)
32 | const hash = hashMatch ? hashMatch[0] : ''
33 | const normalized = normalize(path)
34 |
35 | if (endingSlashRE.test(normalized)) {
36 | return path
37 | }
38 | return normalized + '.html' + hash
39 | }
40 |
41 | export function isActive (route, path) {
42 | const routeHash = route.hash
43 | const linkHash = getHash(path)
44 | if (linkHash && routeHash !== linkHash) {
45 | return false
46 | }
47 | const routePath = normalize(route.path)
48 | const pagePath = normalize(path)
49 | return routePath === pagePath
50 | }
51 |
52 | export function resolvePage (pages, rawPath, base) {
53 | if (base) {
54 | rawPath = resolvePath(rawPath, base)
55 | }
56 | const path = normalize(rawPath)
57 | for (let i = 0; i < pages.length; i++) {
58 | if (normalize(pages[i].path) === path) {
59 | return Object.assign({}, pages[i], {
60 | type: 'page',
61 | path: ensureExt(rawPath)
62 | })
63 | }
64 | }
65 | console.error(`[vuepress] No matching page found for sidebar item "${rawPath}"`)
66 | return {}
67 | }
68 |
69 | function resolvePath (relative, base, append) {
70 | const firstChar = relative.charAt(0)
71 | if (firstChar === '/') {
72 | return relative
73 | }
74 |
75 | if (firstChar === '?' || firstChar === '#') {
76 | return base + relative
77 | }
78 |
79 | const stack = base.split('/')
80 |
81 | // remove trailing segment if:
82 | // - not appending
83 | // - appending to trailing slash (last segment is empty)
84 | if (!append || !stack[stack.length - 1]) {
85 | stack.pop()
86 | }
87 |
88 | // resolve relative path
89 | const segments = relative.replace(/^\//, '').split('/')
90 | for (let i = 0; i < segments.length; i++) {
91 | const segment = segments[i]
92 | if (segment === '..') {
93 | stack.pop()
94 | } else if (segment !== '.') {
95 | stack.push(segment)
96 | }
97 | }
98 |
99 | // ensure leading slash
100 | if (stack[0] !== '') {
101 | stack.unshift('')
102 | }
103 |
104 | return stack.join('/')
105 | }
106 |
107 | export function resolveSidebarItems (page, route, site, localePath) {
108 | const pageSidebarConfig = page.frontmatter.sidebar
109 | if (pageSidebarConfig === 'auto') {
110 | return resolveHeaders(page)
111 | }
112 | const { pages, themeConfig } = site
113 |
114 | const localeConfig = localePath && themeConfig.locales
115 | ? themeConfig.locales[localePath] || themeConfig
116 | : themeConfig
117 |
118 | const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar
119 | if (!sidebarConfig) {
120 | return []
121 | } else {
122 | const { base, config } = resolveMatchingConfig(route, sidebarConfig)
123 | return config
124 | ? config.map(item => resolveItem(item, pages, base))
125 | : []
126 | }
127 | }
128 |
129 | function resolveHeaders (page) {
130 | const headers = groupHeaders(page.headers || [])
131 | return [{
132 | type: 'group',
133 | collapsable: false,
134 | title: page.title,
135 | children: headers.map(h => ({
136 | type: 'auto',
137 | title: h.title,
138 | basePath: page.path,
139 | path: page.path + '#' + h.slug,
140 | children: h.children || []
141 | }))
142 | }]
143 | }
144 |
145 | export function groupHeaders (headers) {
146 | // group h3s under h2
147 | headers = headers.map(h => Object.assign({}, h))
148 | let lastH2
149 | headers.forEach(h => {
150 | if (h.level === 2) {
151 | lastH2 = h
152 | } else if (lastH2) {
153 | (lastH2.children || (lastH2.children = [])).push(h)
154 | }
155 | })
156 | return headers.filter(h => h.level === 2)
157 | }
158 |
159 | export function resolveNavLinkItem (linkItem) {
160 | return Object.assign(linkItem, {
161 | type: linkItem.items && linkItem.items.length ? 'links' : 'link'
162 | })
163 | }
164 |
165 | export function resolveMatchingConfig (route, config) {
166 | if (Array.isArray(config)) {
167 | return {
168 | base: '/',
169 | config: config
170 | }
171 | }
172 | for (const base in config) {
173 | if (ensureEndingSlash(route.path).indexOf(base) === 0) {
174 | return {
175 | base,
176 | config: config[base]
177 | }
178 | }
179 | }
180 | return {}
181 | }
182 |
183 | function ensureEndingSlash (path) {
184 | return /(\.html|\/)$/.test(path)
185 | ? path
186 | : path + '/'
187 | }
188 |
189 | function resolveItem (item, pages, base, isNested) {
190 | if (typeof item === 'string') {
191 | return resolvePage(pages, item, base)
192 | } else if (Array.isArray(item)) {
193 | return Object.assign(resolvePage(pages, item[0], base), {
194 | title: item[1]
195 | })
196 | } else {
197 | if (isNested) {
198 | console.error(
199 | '[vuepress] Nested sidebar groups are not supported. ' +
200 | 'Consider using navbar + categories instead.'
201 | )
202 | }
203 | const children = item.children || []
204 | return {
205 | type: 'group',
206 | title: item.title,
207 | children: children.map(child => resolveItem(child, pages, base, true)),
208 | collapsable: item.collapsable !== false
209 | }
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/lib/build.js:
--------------------------------------------------------------------------------
1 | module.exports = async function build (sourceDir, cliOptions = {}) {
2 | process.env.NODE_ENV = 'production'
3 |
4 | const fs = require('fs-extra')
5 | const path = require('path')
6 | const chalk = require('chalk')
7 | const webpack = require('webpack')
8 | const readline = require('readline')
9 | const escape = require('escape-html')
10 |
11 | const prepare = require('./prepare')
12 | const createClientConfig = require('./webpack/createClientConfig')
13 | const createServerConfig = require('./webpack/createServerConfig')
14 | const { createBundleRenderer } = require('vue-server-renderer')
15 | const { normalizeHeadTag, applyUserWebpackConfig } = require('./util')
16 |
17 | process.stdout.write('Extracting site metadata...')
18 | const options = await prepare(sourceDir)
19 | if (cliOptions.outDir) {
20 | options.outDir = cliOptions.outDir
21 | }
22 |
23 | const { outDir } = options
24 | await fs.remove(outDir)
25 |
26 | let clientConfig = createClientConfig(options, cliOptions).toConfig()
27 | let serverConfig = createServerConfig(options, cliOptions).toConfig()
28 |
29 | // apply user config...
30 | const userConfig = options.siteConfig.configureWebpack
31 | if (userConfig) {
32 | clientConfig = applyUserWebpackConfig(userConfig, clientConfig, false)
33 | serverConfig = applyUserWebpackConfig(userConfig, serverConfig, true)
34 | }
35 |
36 | // compile!
37 | const stats = await compile([clientConfig, serverConfig])
38 |
39 | const serverBundle = require(path.resolve(outDir, 'manifest/server.json'))
40 | const clientManifest = require(path.resolve(outDir, 'manifest/client.json'))
41 |
42 | // remove manifests after loading them.
43 | await fs.remove(path.resolve(outDir, 'manifest'))
44 |
45 | // find and remove empty style chunk caused by
46 | // https://github.com/webpack-contrib/mini-css-extract-plugin/issues/85
47 | // TODO remove when it's fixed
48 | await workaroundEmptyStyleChunk()
49 |
50 | // create server renderer using built manifests
51 | const renderer = createBundleRenderer(serverBundle, {
52 | clientManifest,
53 | runInNewContext: false,
54 | inject: false,
55 | template: await fs.readFile(path.resolve(__dirname, 'app/index.ssr.html'), 'utf-8')
56 | })
57 |
58 | // pre-render head tags from user config
59 | const userHeadTags = (options.siteConfig.head || []).map(renderHeadTag).join('\n ')
60 |
61 | // render pages
62 | console.log('Rendering static HTML...')
63 | for (const page of options.siteData.pages) {
64 | await renderPage(page)
65 | }
66 |
67 | // if the user does not have a custom 404.md, generate the theme's default
68 | if (!options.siteData.pages.some(p => p.path === '/404.html')) {
69 | await renderPage({ path: '/404.html' })
70 | }
71 |
72 | readline.clearLine(process.stdout, 0)
73 | readline.cursorTo(process.stdout, 0)
74 |
75 | if (options.siteConfig.serviceWorker) {
76 | console.log('Generating service worker...')
77 | const wbb = require('workbox-build')
78 | wbb.generateSW({
79 | swDest: path.resolve(outDir, 'service-worker.js'),
80 | globDirectory: outDir,
81 | globPatterns: ['**/*.{js,css,html,png,jpg,jpeg,gif,svg,woff,woff2,eot,ttf,otf}']
82 | })
83 | }
84 |
85 | // DONE.
86 | const relativeDir = path.relative(process.cwd(), outDir)
87 | console.log(`\n${chalk.green('Success!')} Generated static files in ${chalk.cyan(relativeDir)}.`)
88 |
89 | // --- helpers ---
90 |
91 | function compile (config) {
92 | return new Promise((resolve, reject) => {
93 | webpack(config, (err, stats) => {
94 | if (err) {
95 | return reject(err)
96 | }
97 | if (stats.hasErrors()) {
98 | stats.toJson().errors.forEach(err => {
99 | console.error(err)
100 | })
101 | reject(new Error(`Failed to compile with errors.`))
102 | return
103 | }
104 | resolve(stats.toJson({ modules: false }))
105 | })
106 | })
107 | }
108 |
109 | function renderHeadTag (tag) {
110 | const { tagName, attributes, innerHTML, closeTag } = normalizeHeadTag(tag)
111 | return `<${tagName}${renderAttrs(attributes)}>${innerHTML}${closeTag ? `${tagName}>` : ``}`
112 | }
113 |
114 | function renderAttrs (attrs = {}) {
115 | const keys = Object.keys(attrs)
116 | if (keys.length) {
117 | return ' ' + keys.map(name => `${name}="${escape(attrs[name])}"`).join(' ')
118 | } else {
119 | return ''
120 | }
121 | }
122 |
123 | async function renderPage (page) {
124 | const pagePath = page.path
125 | readline.clearLine(process.stdout, 0)
126 | readline.cursorTo(process.stdout, 0)
127 | process.stdout.write(`Rendering page: ${pagePath}`)
128 |
129 | const pageMeta = renderPageMeta(page.frontmatter && page.frontmatter.meta)
130 | const context = {
131 | url: pagePath,
132 | userHeadTags,
133 | pageMeta,
134 | title: 'VuePress',
135 | lang: 'en',
136 | description: ''
137 | }
138 |
139 | let html
140 | try {
141 | html = await renderer.renderToString(context)
142 | } catch (e) {
143 | console.error(chalk.red(`Error rendering ${pagePath}:`))
144 | throw e
145 | }
146 | const filename = pagePath.replace(/\/$/, '/index.html').replace(/^\//, '')
147 | const filePath = path.resolve(outDir, filename)
148 | await fs.ensureDir(path.dirname(filePath))
149 | await fs.writeFile(filePath, html)
150 | }
151 |
152 | function renderPageMeta (meta) {
153 | if (!meta) return ''
154 | return meta
155 | .map(m => {
156 | let res = ` {
158 | res += ` ${key}="${escape(m[key])}"`
159 | })
160 | return res + `>`
161 | })
162 | .join('')
163 | }
164 |
165 | async function workaroundEmptyStyleChunk () {
166 | const styleChunk = stats.children[0].assets.find(a => {
167 | return /styles\.\w{8}\.js$/.test(a.name)
168 | })
169 | if (!styleChunk) return
170 | const styleChunkPath = path.resolve(outDir, styleChunk.name)
171 | const styleChunkContent = await fs.readFile(styleChunkPath, 'utf-8')
172 | await fs.remove(styleChunkPath)
173 | // prepend it to app.js.
174 | // this is necessary for the webpack runtime to work properly.
175 | const appChunk = stats.children[0].assets.find(a => {
176 | return /app\.\w{8}\.js$/.test(a.name)
177 | })
178 | const appChunkPath = path.resolve(outDir, appChunk.name)
179 | const appChunkContent = await fs.readFile(appChunkPath, 'utf-8')
180 | await fs.writeFile(appChunkPath, styleChunkContent + appChunkContent)
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/docs/about/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 关于我 · Yusen's Blog · 学习弯道超车的技巧!
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | 关于
23 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/lib/webpack/createBaseConfig.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = function createBaseConfig (
4 | { siteConfig, sourceDir, outDir, publicPath, themePath, notFoundPath, isAlgoliaSearch, markdown },
5 | { debug } = {},
6 | isServer
7 | ) {
8 | const Config = require('webpack-chain')
9 | const { VueLoaderPlugin } = require('vue-loader')
10 | const CSSExtractPlugin = require('mini-css-extract-plugin')
11 |
12 | const isProd = process.env.NODE_ENV === 'production'
13 | const inlineLimit = 10000
14 |
15 | const config = new Config()
16 |
17 | config
18 | .mode(isProd && !debug ? 'production' : 'development')
19 | .output.path(outDir)
20 | .filename(isProd ? 'assets/js/[name].[chunkhash:8].js' : 'assets/js/[name].js')
21 | .publicPath(isProd ? publicPath : '/')
22 |
23 | if (debug) {
24 | config.devtool('source-map')
25 | } else if (!isProd) {
26 | config.devtool('cheap-module-eval-source-map')
27 | }
28 |
29 | config.resolve
30 | .set('symlinks', true)
31 | .alias.set('@theme', themePath)
32 | .set('@notFound', notFoundPath)
33 | .set('@source', sourceDir)
34 | .set('@app', path.resolve(__dirname, '../app'))
35 | .set('@temp', path.resolve(__dirname, '../app/.temp'))
36 | .set('@default-theme', path.resolve(__dirname, '../default-theme'))
37 | .set(
38 | '@AlgoliaSearchBox',
39 | isAlgoliaSearch ? path.resolve(__dirname, '../default-theme/AlgoliaSearchBox.vue') : path.resolve(__dirname, '../noop.js')
40 | )
41 | .end()
42 | .extensions.merge(['.js', '.jsx', '.vue', '.json'])
43 | .end()
44 | .modules // prioritize our own
45 | .add(path.resolve(__dirname, '../../node_modules'))
46 | .add(path.resolve(__dirname, '../../../'))
47 | .add('node_modules')
48 |
49 | config.resolveLoader
50 | .set('symlinks', true)
51 | .modules // prioritize our own
52 | .add(path.resolve(__dirname, '../../node_modules'))
53 | .add(path.resolve(__dirname, '../../../'))
54 | .add('node_modules')
55 |
56 | config.module.noParse(/^(vue|vue-router|vuex|vuex-router-sync)$/)
57 |
58 | config.module
59 | .rule('vue')
60 | .test(/\.vue$/)
61 | .use('vue-loader')
62 | .loader('vue-loader')
63 | .options({
64 | compilerOptions: {
65 | preserveWhitespace: false
66 | }
67 | })
68 |
69 | config.module
70 | .rule('pug')
71 | .test(/\.pug$/)
72 | .use('pug-plain-loader')
73 | .loader('pug-plain-loader')
74 | .end()
75 |
76 | if (!siteConfig.evergreen) {
77 | const libDir = path.join(__dirname, '..')
78 | config.module
79 | .rule('js')
80 | .test(/\.js$/)
81 | .exclude.add(filepath => {
82 | // Always transpile lib directory
83 | if (filepath.startsWith(libDir)) {
84 | return false
85 | }
86 | // Don't transpile node_modules
87 | return /node_modules/.test(filepath)
88 | })
89 | .end()
90 | .use('buble-loader')
91 | .loader('buble-loader')
92 | .options({
93 | objectAssign: 'Object.assign'
94 | })
95 | }
96 |
97 | config.module
98 | .rule('markdown')
99 | .test(/\.md$/)
100 | .use('vue-loader')
101 | .loader('vue-loader')
102 | .options({
103 | compilerOptions: {
104 | preserveWhitespace: false
105 | }
106 | })
107 | .end()
108 | .use('markdown-loader')
109 | .loader(require.resolve('./markdownLoader'))
110 | .options({
111 | sourceDir,
112 | markdown
113 | })
114 |
115 | config.module
116 | .rule('images')
117 | .test(/\.(png|jpe?g|gif)(\?.*)?$/)
118 | .use('url-loader')
119 | .loader('url-loader')
120 | .options({
121 | limit: inlineLimit,
122 | name: `assets/img/[name].[hash:8].[ext]`
123 | })
124 |
125 | // do not base64-inline SVGs.
126 | // https://github.com/facebookincubator/create-react-app/pull/1180
127 | config.module
128 | .rule('svg')
129 | .test(/\.(svg)(\?.*)?$/)
130 | .use('file-loader')
131 | .loader('file-loader')
132 | .options({
133 | name: `assets/img/[name].[hash:8].[ext]`
134 | })
135 |
136 | config.module
137 | .rule('media')
138 | .test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/)
139 | .use('url-loader')
140 | .loader('url-loader')
141 | .options({
142 | limit: inlineLimit,
143 | name: `assets/media/[name].[hash:8].[ext]`
144 | })
145 |
146 | config.module
147 | .rule('fonts')
148 | .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i)
149 | .use('url-loader')
150 | .loader('url-loader')
151 | .options({
152 | limit: inlineLimit,
153 | name: `assets/fonts/[name].[hash:8].[ext]`
154 | })
155 |
156 | function createCSSRule (lang, test, loader, options) {
157 | const baseRule = config.module.rule(lang).test(test)
158 | const modulesRule = baseRule.oneOf('modules').resourceQuery(/module/)
159 | const normalRule = baseRule.oneOf('normal')
160 |
161 | applyLoaders(modulesRule, true)
162 | applyLoaders(normalRule, false)
163 |
164 | function applyLoaders (rule, modules) {
165 | if (!isServer) {
166 | if (isProd) {
167 | rule.use('extract-css-loader').loader(CSSExtractPlugin.loader)
168 | } else {
169 | rule.use('vue-style-loader').loader('vue-style-loader')
170 | }
171 | }
172 |
173 | rule
174 | .use('css-loader')
175 | .loader('css-loader')
176 | .options({
177 | modules,
178 | localIdentName: `[local]_[hash:base64:8]`,
179 | importLoaders: 1
180 | })
181 |
182 | rule
183 | .use('postcss-loader')
184 | .loader('postcss-loader')
185 | .options(
186 | Object.assign(
187 | {
188 | plugins: [require('autoprefixer')],
189 | sourceMap: !isProd
190 | },
191 | siteConfig.postcss
192 | )
193 | )
194 |
195 | if (loader) {
196 | rule
197 | .use(loader)
198 | .loader(loader)
199 | .options(options)
200 | }
201 | }
202 | }
203 |
204 | createCSSRule('css', /\.css$/)
205 | createCSSRule('scss', /\.scss$/, 'sass-loader', siteConfig.scss)
206 | createCSSRule('sass', /\.sass$/, 'sass-loader', Object.assign({ indentedSyntax: true }, siteConfig.sass))
207 | createCSSRule('less', /\.less$/, 'less-loader', siteConfig.less)
208 | createCSSRule(
209 | 'stylus',
210 | /\.styl(us)?$/,
211 | 'stylus-loader',
212 | Object.assign(
213 | {
214 | preferPathResolver: 'webpack'
215 | },
216 | siteConfig.stylus
217 | )
218 | )
219 |
220 | config.plugin('vue-loader').use(VueLoaderPlugin)
221 |
222 | if (isProd && !isServer) {
223 | config.plugin('extract-css').use(CSSExtractPlugin, [
224 | {
225 | filename: 'assets/css/styles.[chunkhash:8].css'
226 | }
227 | ])
228 |
229 | // ensure all css are extracted together.
230 | // since most of the CSS will be from the theme and very little
231 | // CSS will be from async chunks
232 | config.optimization.splitChunks({
233 | cacheGroups: {
234 | styles: {
235 | name: 'styles',
236 | // necessary to ensure async chunks are also extracted
237 | test: m => /css-extract/.test(m.type),
238 | chunks: 'all',
239 | enforce: true
240 | }
241 | }
242 | })
243 | }
244 |
245 | // inject constants
246 | config.plugin('injections').use(require('webpack/lib/DefinePlugin'), [
247 | {
248 | BASE_URL: JSON.stringify(siteConfig.base || '/'),
249 | GA_ID: siteConfig.ga ? JSON.stringify(siteConfig.ga) : false,
250 | SW_ENABLED: !!siteConfig.serviceWorker
251 | }
252 | ])
253 |
254 | return config
255 | }
256 |
--------------------------------------------------------------------------------