├── docs
├── .vuepress
│ ├── public
│ │ ├── CNAME
│ │ ├── logo.png
│ │ ├── team.png
│ │ ├── sddefault.jpg
│ │ ├── authors
│ │ │ ├── ulivz.png
│ │ │ ├── pigcan.png
│ │ │ └── jothy1023.png
│ │ └── hy-back-text.png
│ ├── theme
│ │ ├── index.js
│ │ ├── global-components
│ │ │ ├── CardLink.vue
│ │ │ ├── Center.vue
│ │ │ ├── Highlight.vue
│ │ │ ├── Black.vue
│ │ │ └── HeadingCorner.vue
│ │ ├── styles
│ │ │ ├── palette.styl
│ │ │ ├── index.styl
│ │ │ └── font.css
│ │ ├── components
│ │ │ ├── RSS.vue
│ │ │ ├── NavLink.vue
│ │ │ ├── EditIcon.vue
│ │ │ ├── FeedbackIcon.vue
│ │ │ ├── Author.vue
│ │ │ ├── Navbar.vue
│ │ │ ├── Home.vue
│ │ │ └── Recommend.vue
│ │ └── layouts
│ │ │ ├── Post.vue
│ │ │ ├── BlogSimpleLayout.vue
│ │ │ ├── IndexPost.vue
│ │ │ ├── BlogPostPage.vue
│ │ │ ├── BlogBlockLayout.vue
│ │ │ ├── Layout.vue
│ │ │ └── Team.vue
│ ├── redirects
│ ├── components
│ │ ├── OtherComponent.vue
│ │ ├── Foo
│ │ │ └── Bar.vue
│ │ └── demo-component.vue
│ ├── enhanceApp.js
│ └── config.js
├── team
│ └── README.md
├── translations
│ └── _posts
│ │ └── 2020-3-27-webpack-5-module-federation
│ │ ├── assets
│ │ ├── main.png
│ │ ├── marais.png
│ │ ├── example-1.png
│ │ ├── example-2.png
│ │ ├── example-3.png
│ │ ├── example-4.png
│ │ ├── zack-jackson.png
│ │ ├── zack-jackson-tweet.png
│ │ ├── kevin-saldana-tweet.png
│ │ ├── zack-jackson-tweet-2.png
│ │ ├── zack-jackson-tweet-3.png
│ │ └── zack-jackson-tweet-4.png
│ │ └── index.md
├── README.md
├── about
│ └── README.md
└── blog
│ └── _posts
│ ├── 2019-12-02-invitation.md
│ └── 2019-12-21-explore-the-way-of-real-time-build-based-on-browser.md
├── .gitignore
├── README.md
├── package.json
└── .github
└── workflows
└── github-pages.yml
/docs/.vuepress/public/CNAME:
--------------------------------------------------------------------------------
1 | richlab.design
2 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@vuepress/theme-default'
3 | }
4 |
--------------------------------------------------------------------------------
/docs/team/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: Team
3 | title: 和喜欢的人一起,做人人都喜欢的产品
4 | ---
5 |
6 | # 和喜欢的人一起,做人人都喜欢的产品
--------------------------------------------------------------------------------
/docs/.vuepress/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/.vuepress/public/logo.png
--------------------------------------------------------------------------------
/docs/.vuepress/public/team.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/.vuepress/public/team.png
--------------------------------------------------------------------------------
/docs/.vuepress/public/sddefault.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/.vuepress/public/sddefault.jpg
--------------------------------------------------------------------------------
/docs/.vuepress/redirects:
--------------------------------------------------------------------------------
1 | /2020/03/27/webpack-5-module-federation/ /translations/2020/03/27/webpack-5-module-federation/
2 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/OtherComponent.vue:
--------------------------------------------------------------------------------
1 |
2 | This is another component
3 |
4 |
--------------------------------------------------------------------------------
/docs/.vuepress/public/authors/ulivz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/.vuepress/public/authors/ulivz.png
--------------------------------------------------------------------------------
/docs/.vuepress/public/hy-back-text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/.vuepress/public/hy-back-text.png
--------------------------------------------------------------------------------
/docs/.vuepress/public/authors/pigcan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/.vuepress/public/authors/pigcan.png
--------------------------------------------------------------------------------
/docs/.vuepress/public/authors/jothy1023.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/.vuepress/public/authors/jothy1023.png
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/main.png
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/marais.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/marais.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | pids
2 | logs
3 | yarn.lock
4 | node_modules
5 | npm-debug.log
6 | coverage/
7 | run
8 | dist
9 | .DS_Store
10 | .nyc_output
11 | .basement
12 | config.local.js
13 | basement_dist
14 | .temp
15 |
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/example-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/example-1.png
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/example-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/example-2.png
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/example-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/example-3.png
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/example-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/example-4.png
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/zack-jackson.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/zack-jackson.png
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/zack-jackson-tweet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/zack-jackson-tweet.png
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/kevin-saldana-tweet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/kevin-saldana-tweet.png
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/zack-jackson-tweet-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/zack-jackson-tweet-2.png
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/zack-jackson-tweet-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/zack-jackson-tweet-3.png
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/zack-jackson-tweet-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rich-lab/blog/HEAD/docs/translations/_posts/2020-3-27-webpack-5-module-federation/assets/zack-jackson-tweet-4.png
--------------------------------------------------------------------------------
/docs/.vuepress/theme/global-components/CardLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/Foo/Bar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ msg }}
4 |
5 |
6 |
7 |
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RichLab's world
2 |
3 | - [Home](https://richlab.design/)
4 | - [About](https://richlab.design/about/)
5 | - [Blog](https://richlab.design/blog/)
6 | - [Translations](https://richlab.design/translations/)
7 |
8 | ## Development
9 |
10 | ```bash
11 | yarn dev
12 | yarn build
13 | ```
14 |
--------------------------------------------------------------------------------
/docs/.vuepress/components/demo-component.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ msg }}
4 |
5 |
6 |
7 |
16 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/global-components/Center.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/global-components/Highlight.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | ---
2 | home: true
3 | heroImage: https://v1.vuepress.vuejs.org/hero.png
4 | tagline: RichLab's Blog.
5 | actionText: Quick Start →
6 | actionLink: /guide/
7 | features:
8 | - title: Feature 1 Title
9 | details: Feature 1 Description
10 | - title: Feature 2 Title
11 | details: Feature 2 Description
12 | - title: Feature 3 Title
13 | details: Feature 3 Description
14 | footer: Made by 完颜 with ❤️
15 | ---
16 |
--------------------------------------------------------------------------------
/docs/about/README.md:
--------------------------------------------------------------------------------
1 | # About
2 |
3 |
4 |
5 |
6 |
7 |
8 | Hello,欢迎来到 **RichLab · 荔枝实验室** 的空间,我们是一群来自[花呗借呗](https://zhuanlan.zhihu.com/p/94875328) 的前端工程师。
9 |
10 | 在这里,我们会分享 3D、基础构建等前端技术,以及摄影、音乐、旅行和美食。
11 |
12 | **恩,做这个站点的初衷是 “表达”, 我们期望能够创造更多的 “连接”。**
13 |
14 | 接下来,你可以移步 [博客](../blog/) 或者我们的 [翻译专区](../translations/),也可以在下面了解更多关于我们的消息:
15 |
16 | - [知乎专栏](https://zhuanlan.zhihu.com/richlab)
17 | - [语雀](https://www.yuque.com/richlab)
18 |
--------------------------------------------------------------------------------
/docs/.vuepress/enhanceApp.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Client app enhancement file.
3 | *
4 | * https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements
5 | */
6 | import './theme/styles/font.css'
7 |
8 | export default ({
9 | Vue, // the version of Vue being used in the VuePress app
10 | options, // the options for the root Vue instance
11 | router, // the router instance for the app
12 | siteData // site metadata
13 | }) => {
14 | // ...apply enhancements for the site.
15 | }
16 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/styles/palette.styl:
--------------------------------------------------------------------------------
1 | /**
2 | * RichLab's Palette
3 | * Ref: https://github.com/rich-lab/art/tree/master/vi#palette
4 | */
5 | $colorLight = rgba(252, 252, 252, 1);
6 | $colorGray = rgba(238, 238, 238, 1);
7 | $colorInactive = rgba(119, 119, 119, 1);
8 | $colorFont = rgba(39, 39, 39, 1);
9 | $colorDark = rgba(17, 17, 17, 1);
10 |
11 | /**
12 | * Custom palette here.
13 | *
14 | * ref:https://v1.vuepress.vuejs.org/zh/config/#palette-styl
15 | */
16 | $accentColor = #3B48CE
17 | $textColor = $colorFont
18 | $borderColor = #eaecef
19 | $codeBgColor = #282c34
20 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/components/RSS.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
19 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/layouts/Post.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
29 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/global-components/Black.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
38 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/layouts/BlogSimpleLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
24 |
25 |
42 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/components/NavLink.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ item.text }}
9 |
17 |
18 |
19 |
20 |
21 |
22 |
52 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/global-components/HeadingCorner.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "richlab-blog",
3 | "version": "0.0.1",
4 | "description": "RichLab - 荔枝实验室™️ ️· Ant Financial Huabei & Jiebei Front-end Team.",
5 | "main": "index.js",
6 | "authors": {
7 | "name": "完颜",
8 | "email": "wanyan.wz@antfin.com"
9 | },
10 | "repository": "/richlab-blog",
11 | "scripts": {
12 | "dev": "vuepress dev docs --temp .temp",
13 | "build": "vuepress build docs --temp .temp"
14 | },
15 | "license": "MIT",
16 | "devDependencies": {
17 | "@vuepress-reco/vuepress-plugin-comments": "^1.0.13",
18 | "@vuepress/plugin-back-to-top": "^1.4.0",
19 | "@vuepress/plugin-blog": "^1.9.2",
20 | "@vuepress/plugin-google-analytics": "^1.4.0",
21 | "@vuepress/plugin-html-redirect": "^0.1.2",
22 | "@vuepress/plugin-medium-zoom": "^1.4.0",
23 | "lodash.shuffle": "^4.2.0",
24 | "lodash.throttle": "^4.1.1",
25 | "vuepress": "^1.4.0",
26 | "vuepress-plugin-cat": "^1.0.3",
27 | "vuepress-plugin-code-copy": "^1.0.6",
28 | "vuepress-plugin-comment": "^0.7.3",
29 | "vuepress-plugin-dynamic-base": "^0.1.1",
30 | "vuepress-plugin-feed": "^0.1.8",
31 | "vuepress-plugin-sitemap": "^2.3.1"
32 | },
33 | "dependencies": {
34 | "tailwindcss": "^1.2.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/components/EditIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/components/FeedbackIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/layouts/IndexPost.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
43 |
44 |
70 |
--------------------------------------------------------------------------------
/.github/workflows/github-pages.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the action will run. Triggers the workflow on push or pull request
6 | # events but only for the master branch
7 | on:
8 | push:
9 | branches: [ master ]
10 |
11 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
12 | jobs:
13 | # This workflow contains a single job called "build"
14 | build:
15 | # The type of runner that the job will run on
16 | runs-on: ubuntu-latest
17 |
18 | # Steps represent a sequence of tasks that will be executed as part of the job
19 | steps:
20 | # 切代码到 runner
21 | - uses: actions/checkout@v1
22 | with:
23 | submodules: true
24 | # 下载 git submodule
25 | - uses: srt32/git-actions@v0.0.3
26 | with:
27 | args: git submodule update --init --recursive
28 | # 使用 node:10
29 | - name: use Node.js 10.x
30 | uses: actions/setup-node@v1
31 | with:
32 | node-version: 10.x
33 | - name: Vuepress to GitHub Pages
34 | uses: testthedocs/github-pages-deploy-action@0.0.2
35 | env:
36 | ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
37 | BUILD_DIR: docs/.vuepress/dist # The folder, the action should deploy.
38 | BUILD_SCRIPT: yarn && yarn build # The build script the action should run prior to deploying.
39 | # 设置阿里云OSS的 id/secret,存储到 github 的 secrets 中
40 | - name: setup aliyun oss
41 | uses: manyuanrong/setup-ossutil@master
42 | with:
43 | endpoint: oss-cn-hangzhou.aliyuncs.com
44 | access-key-id: ${{ secrets.OSS_KEY_ID }}
45 | access-key-secret: ${{ secrets.OSS_KEY_SECRET }}
46 | - name: cp files to aliyun
47 | run: ossutil cp -rf docs/.vuepress/dist oss://richlab/
48 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/layouts/BlogPostPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 | {{ apage.title }}
10 | {{ apage.frontmatter.description }}
11 |
12 |
13 |
14 |
15 |
29 |
30 |
111 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/components/Author.vue:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
24 |
61 |
62 |
117 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/layouts/BlogBlockLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
31 |
32 |
119 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/layouts/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
75 |
76 |
109 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/components/Navbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
15 |
18 | {{ $themeConfig.title }}
19 |
20 |
21 |
22 |
36 |
37 |
38 |
39 |
88 |
89 |
141 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/components/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
Oh, hello, nice to meet you!
13 |
WELCOME TO RichLab's WORLD!
14 |
Professional & Creative & Freedom & Love.
15 |
16 |
17 |
18 |
19 |
20 |
21 |
58 |
59 |
177 |
--------------------------------------------------------------------------------
/docs/.vuepress/config.js:
--------------------------------------------------------------------------------
1 | const { description } = require('../../package')
2 |
3 | module.exports = {
4 | /**
5 | * Ref:https://v1.vuepress.vuejs.org/config/#title
6 | */
7 | title: 'RichLab',
8 | /**
9 | * Ref:https://v1.vuepress.vuejs.org/config/#description
10 | */
11 | description: description,
12 | /**
13 | * Extra tags to be injected to the page HTML `
`
14 | *
15 | * ref:https://v1.vuepress.vuejs.org/config/#head
16 | */
17 | head: [
18 | ['link', {
19 | rel: 'icon',
20 | href: 'https://gw.alipayobjects.com/zos/finxbff/compress-tinypng/7a6ec1c9-2680-4817-8748-eeab10cb1469.png'
21 | }],
22 | ['meta', { name: 'referrer', content: 'no-referrer' }],
23 | ['meta', { name: 'theme-color', content: '#272727' }],
24 | ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }],
25 | ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }]
26 | ],
27 |
28 | postcss: {
29 | plugins: [
30 | require('tailwindcss'),
31 | require('autoprefixer'),
32 | ]
33 | },
34 |
35 | /**
36 | * Theme configuration, here is the default theme configuration for VuePress.
37 | *
38 | * ref:https://v1.vuepress.vuejs.org/theme/default-theme-config.html
39 | */
40 | themeConfig: {
41 | repo: '',
42 | editLinks: false,
43 | docsDir: '',
44 | editLinkText: '',
45 | lastUpdated: false,
46 | logo: 'https://gw.alipayobjects.com/zos/finxbff/compress-tinypng/b1a25d04-6a13-4bb5-a929-a5f0db08b8de.png',
47 | nav: [
48 | {
49 | text: 'Home',
50 | link: '/',
51 | },
52 | {
53 | text: 'About',
54 | link: '/about/'
55 | },
56 | {
57 | text: 'Blog',
58 | link: '/blog/'
59 | },
60 | {
61 | text: 'Translations',
62 | link: '/translations/'
63 | },
64 | {
65 | text: 'Team',
66 | link: '/team/'
67 | },
68 | {
69 | text: 'Github',
70 | link: 'https://github.com/rich-lab'
71 | },
72 | // {
73 | // text: 'Artworks',
74 | // link: 'https://github.com/rich-lab/art/'
75 | // }
76 | ],
77 | sidebar: {
78 | '/guide/': [
79 | {
80 | title: 'Guide',
81 | collapsable: false,
82 | children: [
83 | '',
84 | 'using-vue',
85 | ]
86 | }
87 | ],
88 | }
89 | },
90 |
91 | /**
92 | * Apply plugins,ref:https://v1.vuepress.vuejs.org/zh/plugin/
93 | */
94 | plugins: [
95 | 'cat',
96 | 'code-copy',
97 | ['sitemap', {
98 | hostname: 'https://richlab.design/',
99 | }],
100 | // ref: https://github.com/webmasterish/vuepress-plugin-feed
101 | ['feed', {
102 | canonical_base: 'https://richlab.design/',
103 | feeds: {
104 | rss2: { enable: true },
105 | atom1: { enable: false },
106 | json1: { enable: false },
107 | },
108 | posts_directories: [
109 | '/blog/_posts',
110 | '/translations/_posts'
111 | ]
112 | }],
113 | ['@vuepress/google-analytics', {
114 | ga: 'UA-128189152-2'
115 | }],
116 | '@vuepress/plugin-back-to-top',
117 | '@vuepress/plugin-medium-zoom',
118 | [
119 | '@vuepress/html-redirect', {
120 | countdown: 0
121 | }
122 | ],
123 | ['@vuepress-reco/comments', {
124 | solution: 'valine',
125 | options: {
126 | appId: 'yPsaft2cSPtwnXW6UiXBS4ld-gzGzoHsz',
127 | appKey: 'AYI4KeXQloUWsuebDElj8A67',
128 | }
129 | }],
130 | [
131 | 'dynamic-base',
132 | {
133 | publicPath: process.env.NETLIFY_CI
134 | ? null
135 | : 'https://richlab.oss-cn-hangzhou.aliyuncs.com/',
136 |
137 | routeBash: {
138 | 'richlab.design': '/',
139 | 'rich-lab.github.io': '/blog/',
140 | }
141 | }
142 | ],
143 | [
144 | '@vuepress/blog',
145 | {
146 | directories: [
147 | {
148 | // Unique ID of current classification
149 | id: 'post',
150 | // Target directory
151 | dirname: 'blog/_posts',
152 | // Path of the `entry page` (or `list page`)
153 | path: '/blog/',
154 | itemPermalink: '/blog/:year/:month/:day/:slug',
155 | // layout: 'Layout',
156 | pagination: {
157 | lengthPerPage: 20,
158 | },
159 | },
160 | {
161 | // Unique ID of current classification
162 | id: 'translations',
163 | // Target directory
164 | dirname: 'translations/_posts',
165 | // Path of the `entry page` (or `list page`)
166 | path: '/translations/',
167 | itemPermalink: '/translations/:year/:month/:day/:slug',
168 | // layout: 'Layout',
169 | pagination: {
170 | lengthPerPage: 20,
171 | },
172 | },
173 | ],
174 | },
175 | ],
176 | ]
177 | }
178 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/components/Recommend.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 更多阅读
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
51 |
52 |
65 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/styles/index.styl:
--------------------------------------------------------------------------------
1 | /**
2 | * Custom Styles here.
3 | *
4 | * ref:https://v1.vuepress.vuejs.org/config/#index-styl
5 | */
6 |
7 | html, body, #app, .theme-container {
8 | position: relative;
9 | width: 100vw;
10 | height: 100vh;
11 | }
12 |
13 | h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
14 | margin-bottom: 0.5rem;
15 | font-family: "Montserrat", sans-serif;
16 | font-weight: 700;
17 | line-height: 1.44;
18 | color: inherit;
19 | text-transform: uppercase;
20 | letter-spacing: 0.2em;
21 |
22 | img {
23 | max-height 1.5rem
24 | }
25 | }
26 |
27 | #app {
28 | .global-ui {
29 | position relative
30 | z-index 100
31 | }
32 |
33 | .home .hero img {
34 | max-height 220px
35 | }
36 |
37 | .content {
38 | img {
39 | max-width 100% !important
40 | }
41 | }
42 |
43 | code {
44 | overflow-wrap break-word
45 |
46 | }
47 |
48 | .navbar {
49 | //border-bottom 1px solid $colorGray
50 | border-bottom: none;
51 | box-shadow 0px 0px 15px 0px rgba(132, 132, 132, 0.2) !important;
52 |
53 | .nav-links {
54 | margin-top: 0.2rem;
55 | }
56 | }
57 |
58 | .search-box {
59 | margin-right 1.5rem
60 |
61 | input {
62 | background: #fff url(https://gw.alipayobjects.com/zos/bmw-prod/72262c89-1d96-442d-bdcf-e4e50d10bd42.svg) 0.1rem 0.3rem no-repeat;
63 | background-size: 1.5rem;
64 | -webkit-appearance: none;
65 | border-radius 0
66 | border-style: solid;
67 | border-style: unset;
68 | border-bottom: 1px solid lighten($colorInactive, 50%);
69 | transition all 0.3s;
70 |
71 | &.focused {
72 | border-bottom: 1px solid $colorFont;
73 | }
74 | }
75 |
76 | .suggestions {
77 | font-size 1rem
78 | padding 0.5rem
79 | display block !important;
80 | font-family: clear;
81 | font-style: normal;
82 | font-weight: lighter;
83 | top: 2rem;
84 | border none;
85 | box-shadow 0px 0px 15px 0px rgba(132, 132, 132, 0.2);
86 |
87 | .page-title {
88 | font-weight: normal;
89 | }
90 |
91 | .header {
92 | font-weight: normal;
93 | }
94 | }
95 | }
96 | }
97 |
98 |
99 | body {
100 | font-weight lighter;
101 | font-family: "Cardo", "Times New Roman", Times, serif;
102 | font-size: 1.1rem;
103 | font-weight: 400;
104 | line-height: 1.5;
105 | color: #212529;
106 | background-color: #fff;
107 | }
108 |
109 |
110 | //.page-edit {
111 | // background-color: $colorGray;
112 | //}
113 | //
114 | //footer {
115 | // background-color: $colorGray;
116 | //}
117 | //
118 | //.custom-page-bottom {
119 | // background-color: $colorGray;
120 | //}
121 |
122 | .icon.outbound {
123 | display: none;
124 | }
125 |
126 |
127 | .sidebar {
128 | font-style italic
129 | }
130 |
131 | .navbar .nav-links {
132 | a {
133 | white-space: nowrap;
134 | position: relative;
135 | border none !important;
136 | color: $colorInactive
137 | }
138 |
139 | a.router-link-active {
140 | color: $colorFont;
141 |
142 | &:after {
143 | content: "";
144 | position: absolute;
145 | top: 170%;
146 | left: -0.2em;
147 | right: -0.2em;
148 | bottom: -75%;
149 | transition background-color 0.2s;
150 | background-color: lighten($colorInactive, 50%);
151 | }
152 | }
153 |
154 | a:hover {
155 | text-decoration: none !important;
156 |
157 | &:after {
158 | background-color: $colorDark;
159 | }
160 | }
161 | }
162 |
163 | .content__default {
164 | h1 {
165 | position: relative;
166 | }
167 |
168 | a {
169 | cursor pointer
170 | @media (min-width: $MQMobile) {
171 | color $colorFont
172 | }
173 | }
174 |
175 |
176 | @media (min-width: $MQMobile) {
177 | a {
178 | max-width 100%
179 | position: relative;
180 | display: inline-block;
181 | vertical-align bottom
182 | }
183 |
184 | a:after {
185 | content: "";
186 | position: absolute;
187 | top: 60%;
188 | left: -0.1em;
189 | right: -0.1em;
190 | bottom: 10%;
191 | transition: top 200ms cubic-bezier(0, 0.8, 0.13, 1);
192 | background-color: alpha($accentColor, 0.2);
193 | }
194 |
195 | a:hover {
196 | text-decoration: none !important;
197 |
198 | &:after {
199 | top: 10%;
200 | }
201 | }
202 | }
203 | }
204 |
205 | p
206 | margin-top: 1.2em;
207 |
208 | blockquote
209 | box-shadow: inset 3px 0 0 0 rgba(0, 0, 0, 0.84);
210 | border-left: none;
211 | margin-top: 2em;
212 | font-style: italic;
213 | color: $colorInactive;
214 | font-size: 21px;
215 | padding-left: 23px;
216 |
217 | @import '~prismjs/themes/prism-tomorrow.css'
218 |
219 | div[class*="language-"]
220 | position: relative;
221 | background-color: $colorFont;
222 | border-radius: 6px;
223 |
224 | .theme-default-content pre, .theme-default-content pre[class*="language-"] {
225 | background white
226 | line-height: 1.4;
227 | padding: 1.25rem 1.5rem;
228 | margin: 0.85rem 0;
229 | background-color: $colorFont;
230 | border-radius: 6px;
231 | overflow: auto;
232 | font-size 1rem;
233 | letter-spacing 0.5px
234 |
235 | .token.atrule, .token.attr-value, .token.keyword {
236 | color: #63DAF5;
237 | }
238 | }
239 |
240 | strong {
241 | font-weight: 900;
242 | }
243 |
244 | h1 a::after {
245 | display none
246 | }
247 |
248 |
249 | // Sidebar defaults to right at Blog Layout
250 | .theme-container.layout-post {
251 | h1 a.header-anchor {
252 | padding-right 0
253 | }
254 |
255 | .sidebar {
256 | font-style normal
257 |
258 | .sidebar-group.depth-0 p {
259 | display none
260 | }
261 |
262 | a.sidebar-link.active {
263 | border-width 1px
264 | }
265 |
266 | .sidebar-group a.sidebar-link {
267 | padding-left: 1.5rem;
268 | }
269 | }
270 |
271 | @media (min-width: $MQMobile) {
272 | .theme-default-content.content__default:not(.custom) > h1 {
273 | margin-top 3rem !important
274 |
275 | &:first-child {
276 | margin-top -1.5rem
277 | margin-bottom 1rem
278 | }
279 | }
280 |
281 | .sidebar {
282 | left auto
283 | right 0
284 | }
285 |
286 | .page {
287 | padding-left: 5rem
288 | padding-right 20rem
289 | }
290 | }
291 | }
292 |
293 |
294 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/layouts/Team.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 和喜欢的人一起
6 |
7 |
8 | 做人人都喜欢的产品
9 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 | 在 RichLab,
23 | 我们打造的前端项目每天都被数百万用户在使用。我们相信,是保持年轻、好玩、认真的心态,让团队富有创造力。这是每一位成员聚集在一起形成的团队基因。
24 |
25 |
26 |
27 |
28 |
29 |
30 |

35 |
36 |
37 |

42 |
43 |
44 |
45 |
46 |
47 | 背靠大舞台
48 |
49 | 「得益于蚂蚁金服花呗借呗亿级用户的金融平台,我们有机会通过自研 2D/3D
50 | 引擎 (Oasis)
51 | 和工作流,开发出动画、游戏等富交互应用,帮助业务获得更高的增长。」
52 |
53 |
54 | —— RichLab 自研 2D/3D 引擎 Oasis 负责人 烧鹅
55 |
56 |
57 |
58 |
59 |
60 |

65 |
66 |
67 |
68 |
79 |
80 |
81 | 乐在双十一
82 |
83 | 每年双十一,我们既在忙碌和紧张中面对稳定性的挑战,又在并肩作战中感受快乐。
84 |
85 |
86 |
87 |
88 |
89 |
90 |
![]()
91 |
92 |
93 |
94 |
95 |
96 | 践行公益
97 |
98 | RichLab
99 | 全员每年践行阿里巴巴「公益时」。从社区服务,到环境清洁,向外界贡献技术之外的价值。
100 |
101 |
102 |
103 |
104 |
108 |
109 |
110 |
111 |
112 |
113 |
![]()
114 |
115 |
116 |
117 |
118 |
119 | 代码之外,各有所长
120 |
121 | 代码不是生活的全部,我们在音乐、越野、运动、旅行等领域都可以大展身手。
122 |
123 |
124 |
125 |
126 |
127 |
128 |
![]()
129 |
130 |
131 |
「{{ hobit.content }}」
132 |
—— {{ hobit.quote }}
133 |
134 |
135 |
136 |
137 |
138 | 成为有趣好玩的一部分
139 | 如果你想成为我们的一份子,请点击 此处。
140 | 我们期待一个独一无二的你。
141 |
142 |
143 |
144 |

149 |
150 |
151 |
154 |
155 |
156 |
157 |
206 |
207 |
246 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/styles/font.css:
--------------------------------------------------------------------------------
1 | /* greek-ext */
2 | @font-face {
3 | font-family: 'Cardo';
4 | font-style: italic;
5 | font-weight: 400;
6 | src: local('Cardo Italic'), local('Cardo-Italic'), url(https://gw.alipayobjects.com/os/bmw-prod/56c968df-dc23-4a18-9f8e-551da2393a59.woff2) format('woff2');
7 | unicode-range: U+1F00-1FFF;
8 | }
9 |
10 | /* greek */
11 | @font-face {
12 | font-family: 'Cardo';
13 | font-style: italic;
14 | font-weight: 400;
15 | src: local('Cardo Italic'), local('Cardo-Italic'), url(https://gw.alipayobjects.com/os/bmw-prod/6b4f9440-6807-4280-9dba-069089e935f3.woff2) format('woff2');
16 | unicode-range: U+0370-03FF;
17 | }
18 |
19 | /* latin-ext */
20 | @font-face {
21 | font-family: 'Cardo';
22 | font-style: italic;
23 | font-weight: 400;
24 | src: local('Cardo Italic'), local('Cardo-Italic'), url(https://gw.alipayobjects.com/os/bmw-prod/16d6554f-021e-467d-8595-8e490d69f771.woff2) format('woff2');
25 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
26 | }
27 |
28 | /* latin */
29 | @font-face {
30 | font-family: 'Cardo';
31 | font-style: italic;
32 | font-weight: 400;
33 | src: local('Cardo Italic'), local('Cardo-Italic'), url(https://gw.alipayobjects.com/os/bmw-prod/093fcc95-d23b-4c04-b088-089116e6f084.woff2) format('woff2');
34 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
35 | }
36 |
37 | /* greek-ext */
38 | @font-face {
39 | font-family: 'Cardo';
40 | font-style: normal;
41 | font-weight: 400;
42 | src: local('Cardo'), local('Cardo-Regular'), url(https://gw.alipayobjects.com/os/bmw-prod/0e92050d-80c2-4e37-a08c-5e59fc1d2706.woff2) format('woff2');
43 | unicode-range: U+1F00-1FFF;
44 | }
45 |
46 | /* greek */
47 | @font-face {
48 | font-family: 'Cardo';
49 | font-style: normal;
50 | font-weight: 400;
51 | src: local('Cardo'), local('Cardo-Regular'), url(https://gw.alipayobjects.com/os/bmw-prod/6c433069-ce53-4bbc-a3d8-215843a3a381.woff2) format('woff2');
52 | unicode-range: U+0370-03FF;
53 | }
54 |
55 | /* latin-ext */
56 | @font-face {
57 | font-family: 'Cardo';
58 | font-style: normal;
59 | font-weight: 400;
60 | src: local('Cardo'), local('Cardo-Regular'), url(https://gw.alipayobjects.com/os/bmw-prod/bf8aa816-7913-4ecd-8c69-94dd8077c0e9.woff2) format('woff2');
61 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
62 | }
63 |
64 | /* latin */
65 | @font-face {
66 | font-family: 'Cardo';
67 | font-style: normal;
68 | font-weight: 400;
69 | src: local('Cardo'), local('Cardo-Regular'), url(https://gw.alipayobjects.com/os/bmw-prod/78c26ab4-1eaa-457e-a17a-de561ec022b7.woff2) format('woff2');
70 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
71 | }
72 |
73 | /* greek-ext */
74 | @font-face {
75 | font-family: 'Cardo';
76 | font-style: normal;
77 | font-weight: 700;
78 | src: local('Cardo Bold'), local('Cardo-Bold'), url(https://gw.alipayobjects.com/os/bmw-prod/0a5a7153-fb38-4448-a919-c749fe0e229a.woff2) format('woff2');
79 | unicode-range: U+1F00-1FFF;
80 | }
81 |
82 | /* greek */
83 | @font-face {
84 | font-family: 'Cardo';
85 | font-style: normal;
86 | font-weight: 700;
87 | src: local('Cardo Bold'), local('Cardo-Bold'), url(https://gw.alipayobjects.com/os/bmw-prod/9b6018ad-68f9-49b5-83e7-59e66207bfe4.woff2) format('woff2');
88 | unicode-range: U+0370-03FF;
89 | }
90 |
91 | /* latin-ext */
92 | @font-face {
93 | font-family: 'Cardo';
94 | font-style: normal;
95 | font-weight: 700;
96 | src: local('Cardo Bold'), local('Cardo-Bold'), url(https://gw.alipayobjects.com/os/bmw-prod/a084a1f8-8252-434e-a418-0ff225753df0.woff2) format('woff2');
97 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
98 | }
99 |
100 | /* latin */
101 | @font-face {
102 | font-family: 'Cardo';
103 | font-style: normal;
104 | font-weight: 700;
105 | src: local('Cardo Bold'), local('Cardo-Bold'), url(https://gw.alipayobjects.com/os/bmw-prod/3b76d59f-6143-42f5-b4e0-fd1721b0d18b.woff2) format('woff2');
106 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
107 | }
108 |
109 | /* cyrillic-ext */
110 | @font-face {
111 | font-family: 'Montserrat';
112 | font-style: normal;
113 | font-weight: 400;
114 | src: local('Montserrat Regular'), local('Montserrat-Regular'), url(https://gw.alipayobjects.com/os/bmw-prod/1e88527f-bdaf-4913-bb6d-4a45771225c5.woff2) format('woff2');
115 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
116 | }
117 |
118 | /* cyrillic */
119 | @font-face {
120 | font-family: 'Montserrat';
121 | font-style: normal;
122 | font-weight: 400;
123 | src: local('Montserrat Regular'), local('Montserrat-Regular'), url(https://gw.alipayobjects.com/os/bmw-prod/3d0ee6fe-8af5-4530-b918-f7ea0095a06e.woff2) format('woff2');
124 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
125 | }
126 |
127 | /* vietnamese */
128 | @font-face {
129 | font-family: 'Montserrat';
130 | font-style: normal;
131 | font-weight: 400;
132 | src: local('Montserrat Regular'), local('Montserrat-Regular'), url(https://gw.alipayobjects.com/os/bmw-prod/e574f448-d194-4271-9208-48f2b5436b52.woff2) format('woff2');
133 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
134 | }
135 |
136 | /* latin-ext */
137 | @font-face {
138 | font-family: 'Montserrat';
139 | font-style: normal;
140 | font-weight: 400;
141 | src: local('Montserrat Regular'), local('Montserrat-Regular'), url(https://gw.alipayobjects.com/os/bmw-prod/bc59b3fc-aca0-4de9-9f69-09207c0c92cf.woff2) format('woff2');
142 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
143 | }
144 |
145 | /* latin */
146 | @font-face {
147 | font-family: 'Montserrat';
148 | font-style: normal;
149 | font-weight: 400;
150 | src: local('Montserrat Regular'), local('Montserrat-Regular'), url(https://gw.alipayobjects.com/os/bmw-prod/f3d6fc2a-245d-49e8-b0be-c51c60cfb3a9.woff2) format('woff2');
151 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
152 | }
153 |
154 | /* cyrillic-ext */
155 | @font-face {
156 | font-family: 'Montserrat';
157 | font-style: normal;
158 | font-weight: 700;
159 | src: local('Montserrat Bold'), local('Montserrat-Bold'), url(https://gw.alipayobjects.com/os/bmw-prod/60a2638c-e932-47c0-b2bd-6c999c39646c.woff2) format('woff2');
160 | unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
161 | }
162 |
163 | /* cyrillic */
164 | @font-face {
165 | font-family: 'Montserrat';
166 | font-style: normal;
167 | font-weight: 700;
168 | src: local('Montserrat Bold'), local('Montserrat-Bold'), url(https://gw.alipayobjects.com/os/bmw-prod/029d0fe8-a325-4098-9ef4-5a2f28bc6c4b.woff2) format('woff2');
169 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
170 | }
171 |
172 | /* vietnamese */
173 | @font-face {
174 | font-family: 'Montserrat';
175 | font-style: normal;
176 | font-weight: 700;
177 | src: local('Montserrat Bold'), local('Montserrat-Bold'), url(https://gw.alipayobjects.com/os/bmw-prod/e4e2180c-dab1-4c44-8747-109c1b786a43.woff2) format('woff2');
178 | unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
179 | }
180 |
181 | /* latin-ext */
182 | @font-face {
183 | font-family: 'Montserrat';
184 | font-style: normal;
185 | font-weight: 700;
186 | src: local('Montserrat Bold'), local('Montserrat-Bold'), url(https://gw.alipayobjects.com/os/bmw-prod/c70e310c-5379-443b-8855-77ba987bd8be.woff2) format('woff2');
187 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
188 | }
189 |
190 | /* latin */
191 | @font-face {
192 | font-family: 'Montserrat';
193 | font-style: normal;
194 | font-weight: 700;
195 | src: local('Montserrat Bold'), local('Montserrat-Bold'), url(https://gw.alipayobjects.com/os/bmw-prod/e79d50c0-4a82-46b4-8973-34536b65839e.woff2) format('woff2');
196 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
197 | }
198 |
--------------------------------------------------------------------------------
/docs/blog/_posts/2019-12-02-invitation.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar: auto
3 | date: 2019-12-02
4 | author: ulivz
5 | hero: https://cdn.nlark.com/yuque/0/2020/jpeg/242808/1585640682302-f7acc307-0764-41a5-bf78-7b636afbed86.jpeg
6 | description: 如果你拥有自己的想法,也有一颗温暖而美好的梦想,那么,RichLab 一定是一个你值得的目的地。
7 | ---
8 |
9 | # 蚂蚁金服 RichLab 团队邀你来聊一聊
10 |
11 | 
12 |
13 | ## 入职那天
14 |
15 | **“请不要被黑暗的手抓住,朝着光的方向奔跑吧。”** —— 这是 RichLab 成员、VuePress 开发者真山喜欢的一句话。
16 |
17 | 可能是离海更远的缘故,杭州的夏天比上海过起来更灼人。那个冒着骄阳烈火、在七月拿到 offer 后就满心欢喜地跑到杭州来看他将来公司的少年,已经悄悄地在西湖区的大街小巷留下了他的足迹 —— 上海於他,也悄悄地在他的回忆里慢慢被封存镶边。
18 |
19 | 
20 |
21 | 八月三十日,是他入职的日子。离开张江幽静的办公室,也已一月有余。他用冷水尝试抹去脸上的疲态,整理好安闲未措的心绪,并将二十四年来获取到的证件一件一件小心翼翼地放到背包内,还有一台崭新的电脑,就这样出门了。
22 |
23 | 那天早上的阳光很耀眼,他一抬头,就不断有树缝里的阳光蹦进他的眼中。他找到了一辆共享自行车,开始向着他的目的地前行着。
24 |
25 | 
26 |
27 | Z 空间很大,但 Z 空间的保安大哥很亲切,告诉了他入职该去的地点。四号大堂的姐姐也很热情地告诉了他下一步的方向。于是,一整天的入职培训开始了。那天,他认识了从一江之隔的滨江网易过来的冷峻小哥,还有好几位从北京和上海来的伙伴 —— 有一位姐姐让他印象深刻,姐姐三十而立,北京人,还有一个几岁的孩子。他问姐姐: "为什么你会跑那么远来杭州,北京不是也有蚂蚁吗?", “因为想来核心的地方看看”,姐姐如是说。在那一刻,他似乎有了一丝共鸣。
28 |
29 | 很快,一天的培训就结束了。下午四时许,他赶上了前往支付宝大楼的班车,再按照之前就测试过的几百米走到了德力西,身着蓝色T恤的皓默师兄下楼来接他,也带他到了工位。他静静地坐下,揭开才领到的新电脑——
30 |
31 | 也,揭开了一段新生活。
32 |
33 | ... ...
34 |
35 | 时光很快,很久很久以后,真山不再是一个新人,他也开始在寻找下一个,和他拥有一样梦想的人。
36 |
37 |
38 |
39 | ## RichLab
40 |
41 | 这是真山所在的团队:**“RichLab 荔枝实验室”**,江湖又称 **『花呗借呗前端技术团队』**,隶属于蚂蚁金服消费信贷事业群 。
42 |
43 | 
其实,真山第一次看到 “微贷” 这个词的时候还是很懵的,他没有贷过款,更不懂微贷是什么,不过,好在他用过花呗。印象中,花呗是一个十分有趣的产品。
44 |
45 |
46 |
47 | ## 很久以后
48 |
49 | 最近,真山采访了 RichLab 的一些经常能够看到的伙伴,让他们聊一聊在琐碎的代码里,有怎样大大的梦想。首先,他采访了 Antd 的孪生兄弟:
**
****
**
这是他第一次离 Antd 这么近,其负责人,人称 “老大爷” 的 @圆非 对他说:
50 |
51 | > “ __Ant Design Mobile 是服务于移动端产品的设计体系,提供基于 React 的一站式解决方案,帮助用户快速高效地搭建一流的产品和服务。__”
52 |
53 |
54 | 这听起来很枯燥,真山心里想,不就是 “移动端的 Antd” 吗?老爷子接着说,“ Ant Design Mobile 正在筹划 3.0,欢迎熟悉 antd 和 React 的 ...”
55 |
56 | 老爷子还没说完,不远处的鹅叔 @烧鹅 听到这里有动静,过来拉走了真山,并向他介绍着一款极具创意和大气的 3D 引擎产品 —— **Oasis 3D**:
57 |

58 |
59 | 接着,鹅叔掏出了手机,打开了花呗双十一主市场,说到:“这个就是 Oasis 驱动的”:
60 |
61 |
62 |
63 |
64 |
65 |
66 | 虽然真山不知道怎么做出来,但他觉得很酷,WebGL,特效,渲染,这些于他只是浅尝辄止的世界,此刻显得有些陌生,不过,接着鹅叔就对他说了一句让他印象深刻的话:
67 |
68 | > “ __Oasis3D 的使命就是要把 3D 开发的荒漠变成绿洲,让每个前端工程师都能轻松开发专业的 3D 应用,让 Web 世界也能达到影视级的绚丽多姿。__”
69 |
70 |
71 | 多么美好的愿景!真山期待着有一天也能轻松地产出这样的动画,同时,这是真山第一次体会到:原来技术人员的梦想可以可以这般绚烂。
72 |
73 | 这让真山想到他过去一年在 RichLab 的一个作品 —— 一个面向蚂蚁前端开发者的营销研发框架 **Sherry**。很久以前,真山这样地聊过 Sherry:
74 |
75 | > “ __我们在开发 Sherry 的过程中,习惯了以用户视角倒过来看,首先思考用户期望的是什么?提示和文档要写成什么样才简洁易懂?当想清楚这些后,Sherry 就拥有了它的灵魂。__”
76 |
77 |
78 | 
79 |
80 | 看着屏幕上朦胧的光与影,真山陷入了沉思,他似乎陷入了对 Sherry 愿景的构思中。
81 |
82 | 突然,“咔嚓”一声,几米开外的 @玄寂 给正在聊天的众人拍了一张照片,似乎引起了真山的注意,于是他走过去,开始跟玄寂交谈起来。
83 |
84 | 他对玄寂的第一印象是,一个技术一流、充满着复古气息的英俊男子,工作中,他研发了一个基于浏览器端的构建和调试方案:
85 |
86 | 
87 |
88 | @玄寂 对该产品的介绍是:
89 |
90 | > “ __Gravity 是一套完全基于浏览器技术实现的预览和调试解决方案。它通过合理的运行时代码编译模式,完整的基于浏览器的文件系统,轻巧的包管理模式,让整个前端的研发模式被完整的移植到了浏览器内。__”
91 |
92 |
93 | 这个产品惊艳到了真山,因为他从来没有相关的经验,他也对此充满了兴趣。
94 |
95 | 更让真山想不到的是,原来玄寂还是一位实实在在的摄影与旅游达人,这和真山的兴趣不谋而合,于是,真山开始听着玄寂畅谈他关于旅行的轶事 —— 那些瞬间的美好,就如同他 VLOG 中的画面一样:
96 |
97 |
98 |
99 | 聊着聊着,一天就这样悄悄地过去了~
100 |
101 |
102 | ## 过去几年
103 |
104 | 从 2017 到 2019,Richlab 的伙伴们游历了很多地方,也留下了很多值得回忆的印象:
105 |
106 | 


107 |
108 |
109 | ## 我们,在寻找独一无二的你
110 |
111 | 很久以后,当真山也不再年轻,他这样形容着 RichLab:
112 |
113 | _“ RichLab 的成员们是一群有趣的人,他们用专业的技术、独特的创意,打磨着那些核心的业务产品,制衡着那些琐碎的工作流程。工作之外,他们也是一个个有血有梦的年轻人,在技术领域做到顶尖,在生活中坚持理想,摇滚、摄影、写作、美食、旅行 ... ... 这些都是他们存在过的缩影。在填饱肚子、照顾好身边的人后,他们都在追逐着每一个人心中独一无二的梦。”_
114 |
115 | 玄寂也曾说:
116 |
117 | _“每一个人,都是一个独立的个体,需要有自己的想法。”_
118 |
119 | 如果你拥有自己的想法,也有一颗温暖而美好的梦想,那么,RichLab 一定是一个你值得的目的地。
120 |
121 | ---
122 |
123 |
124 | 最后,遵循惯例,放上枯燥的联系方式:
125 |
126 | - 简历投递至:haoli.chl@alipay.com (招聘要求详见 [**_JD_**](https://www.yuque.com/richlab/join-us/jd))
127 | - 邮件内备注暗号:RichLab,获得优先评估资格
128 | - 工作地点:杭州、北京、重庆
129 |
130 | 相关问题咨询可直接团队招聘接口人微信:
131 |
132 |
133 |
134 |
135 |
136 | ---
137 |
138 | 最后,放上一些枯燥的陈列:
139 |
140 |
141 | ## 陈列:有趣的团队成员
142 |
143 | | 
[@完颜](https://996.band/) | 
[@ziluo](https://github.com/ziluo) | 
[@djyde](https://github.com/djyde) | 
[@pigcan](https://github.com/pigcan) | 
[@ulivz](https://github.com/ulivz) |
144 | | :---: | :---: | :---: | :---: | :---: |
145 |
146 |
147 | ## 陈列:枯燥的会议
148 |
149 | 在过去的一段时间里,RichLab 的成员们在这些技术会议上,分享过他们和世界连接的想法:
150 |
151 |
152 |
153 |
154 | [_https://myslide.cn/slides/10069_](https://myslide.cn/slides/10069)
155 |
156 |
157 |
158 |
159 |
160 |
161 | [_https://react.w3ctech.com_](https://react.w3ctech.com)
162 |
163 |
164 |
165 |
166 |
167 |
168 | [_https://vue.w3ctech.com/_](https://vue.w3ctech.com/)
169 |
170 |
171 |
172 |
173 |
174 |
175 | [_https://vuefes.jp/2019/sessions/ulivz/_](https://vuefes.jp/2019/sessions/ulivz/)
176 |
177 |
178 |
179 |
180 |
181 |
182 | [_https://seeconf.antfin.com/_](https://seeconf.antfin.com/)
183 |
184 |
185 |
186 |
187 |
188 |
189 | [_http://d2forum.alibaba-inc.com/_](http://d2forum.alibaba-inc.com/)
190 |
191 |
192 |
193 | 
194 |
195 |
--------------------------------------------------------------------------------
/docs/blog/_posts/2019-12-21-explore-the-way-of-real-time-build-based-on-browser.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar: auto
3 | date: 2019-12-21
4 | author: pigcan
5 | hero: https://user-images.githubusercontent.com/848515/71269481-6da1ca00-238a-11ea-8a68-0d2511ec97aa.png
6 | description: 玄寂结合他在蚂蚁多年的构建基础技术的思考,及当下的机遇和挑战,慢慢推演到如何实现 Gravity 这个跑在浏览器中的构建工具
7 | ---
8 |
9 | # 基于浏览器的实时构建探索之路
10 |
11 | ## PPT
12 |
13 | [D2 基于浏览器的实时构建探索之路 - 终稿.pdf](https://github.com/pigcan/blog/files/3989255/D2.-.pdf)
14 |
15 | ## 自我介绍
16 |
17 | 首先先自我介绍下,我是来自 [RichLab 花呗借呗前端团队](https://www.yuque.com/richlab/join-us/invitation) 的同学。在公司大家喊我玄寂,生活中大家称呼我 pigcan 或者猪罐头。除了是一个程序员,我现在也在尝试做一名 [YouTuber](https://www.youtube.com/channel/UCXSC6NZo2q8N2OzvXxp2c1A?view_as=subscriber) 和 [up 主](https://space.bilibili.com/23044280),也在[微信公众号](https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=Mzg2NTA1Nzg1OQ==&scene=124#wechat_redirect)中分享我的生活,我自己的方式践行快乐工作,认真生活。
18 |
19 | ## 体感案例
20 |
21 | 
22 |
23 |
24 | 首先为了让大家有更好的体感,我们先来看一个案例。这个案例是使用code mirror 加 antd tab 组件加 gravity 做的一个实时预览。大家可以通过这个 gif 能看到,我变更js文件或者样式文件的时候,在右侧这个预览区域可以进行实时的更新,那这部分的能力完全由浏览器作为支撑在提供出来,并不涉及本地 server 或者 远程 server 能力的输出。
25 |
26 | 在有了这个体感之后,大家可能会更容易理解我之后讲的内容。
27 |
28 | ## 文章提纲
29 |
30 | 那接下来我会从5个方面来切入来谈一谈基于浏览器的实时构建探索之路。
31 |
32 | - 第一点是背景,从历史来看构建工具每次发生大的变更时,都和前端的技术风潮息息相关,那 2019 年前端界发生的变化,也可以说是促使我做这个技术探索的原因。
33 |
34 | - 有了这些变化,通常情况下现有的技术架构就会可能出现不满足现状的情形,这就是机遇了,这也就是我第二部分想要来说的,基于这些变化对于我们的构建会有哪些机遇,而面对这些机遇,我们在技术上又会有哪些挑战。
35 |
36 | - 第三点我会来谈一下在面对这些机遇和挑战时,我们在技术上的做出的选择,也就是我们如何来架构整个技术方案。
37 |
38 | - 第四点我会来谈一谈基于这种技术架构下会需要克服的技术难点,主要是要抛一些我的解决思路。
39 |
40 | - 第五点也是最后一点,我会来谈一谈这个技术方案可以有的未来,其实更多的是我对他的期待。
41 |
42 |
43 | ## 背景
44 |
45 | 时间回到2011年,那会儿我们前端一直在强调复用性,基于复用性的考虑,我们会把所有的文件尽可能的按照功能维度进行拆分,拆的越小越好,这种追求我称它为粒子化。粒子化的结果是工程的文件会非常非常碎,所以那个时候的构建工具,更多的思路是化零为整,典型工具有 grunt 和 gulp。
46 |
47 | 随着粒子化时代的到来,到 13 年左右很快新的问题出现了,在我看来主要集中在了两个部分:第一个是,传统的拼接脚本的方式开始并不能满足模块化的需求,因为模块之间存在依赖关系,再者还有动态化载入的需求;第二个是那么多功能模块被划分出来了我们放哪里是一个问题,最初 npm 是并没有向前端模块开放的。 所以接下来便出现了模块加载器,和包管理之战。这场战役让我们的前端模块规范变得五花八门,最后好在所有的包落在了 npm 了。所以这个时候的构建工具更多的是抹平模块规范,典型工具 webpack 的出现意义很大一部分就在于此(当然在这个过程中其实还出现了各种基于加载器去做的的定向构建工具和包管理,这里就先不谈了)。
48 |
49 |
50 | 那时间再次回到 2019 年,我们听到了不一样的声音,这些声音都在对抗 bundler 的理念。
51 |
52 | 比较典型的有两篇文章:
53 |
54 | - [Luke Jackson - Don’t Build That App!](https://formidable.com/blog/2019/no-build-step/)
55 | - [Fred K. Schott - A Future Without Webpack](https://www.pika.dev/blog/pika-web-a-future-without-webpack/)
56 |
57 | 为什么会有这些声音,这些声音背后的原因是什么?一方面是因为会新的技术标准的出现,另外一方面也来源于日益陡峭的学习曲线。
58 |
59 | 现在的前端开发要运行一个项目通常我们需要知道:
60 | - 前端构建的概念
61 | - 要知道在琳琅满目的打包工具中做合理选择
62 | - 要知道如何安装开发环境,如何执行构建,如何执行调试
63 | - 要知道如何配置 - webpack、webpack loaders and plugins etc.
64 | - 要知道如何写插件 - babel APIs、webpack APIs etc.
65 | - 如何调试插件
66 | - 如何解决依赖升级 - babel 5 -> 6 -> 7, webpack 1 -> 2 -> 3 -> 4 -> 5
67 |
68 | 反正就是一个字,`“南”`
69 |
70 | 再来看看我们的包管理,以 CRA 为例现在我们要运行起一个 react 应用,我们居然需要附加如此复杂的依赖。
71 |
72 | 
73 |
74 | 在网上也有一些调侃,前端的依赖比黑洞造成的时间扭曲还要大。
75 |
76 | 
77 |
78 |
79 | 回过头再来看,2019 的趋势是什么,相信大家都感觉到了云这个词,我们很多的流程都在上云。那面向上云的这种场景,我们如此复杂的 bundler 和包管理是否符合这种趋势呢?
80 |
81 | 归根结底,其实是要探讨一个问题:`前端资源的加载和分发是不是还会有更好的形式`。
82 |
83 | 面对这个问题,我觉得是有空间的,正是这种笃定,才有了接下来的内容。
84 |
85 | ## 机遇和挑战
86 |
87 | ### 现状
88 | 在上一小节中我们已经谈到了 2019 年不管是 pro / low code 都在朝着上云的趋势在变化,那应对这些变化,我们先来看看现有的一些平台,他们对于构建的态度是什么。
89 |
90 | | 类型 | 代表 |
91 | | ---- | ---- |
92 | | 专业 | Codesandbox、Stackbiltz、Gitlab Web IDE、Ali Cloud IDE |
93 | | 辅助 | Outsystems、Mendix、云凤蝶 |
94 |
95 | 从这些平台中我们可以总结出三种态度:
96 |
97 | - 只做编辑器或者画板
98 | - 做编辑器或者画板并且提供了一个限制性的研发环境
99 | - 做编辑器或者画板并且提供了一个完全开放的研发环境
100 |
101 | 总结下这三种态度,本质上是使用了两种技术方案
102 | - 容器技术
103 | - 基于浏览器的加载策略
104 |
105 | 最终其实可以总结为:
106 |
107 | - 把服务端的能力进行输出。这种方案的优势是服务端拥有和本地研发环境一致化的环境;缺点是即时性较差、效率较差、无法离线、成本高昂。
108 | - 把客户端的能力释放出来。这种方案的优势是无服务端依赖、即时性、高效率、可离线运行;但缺点也比较明显,所有能力建设都必须围绕着浏览器技术
109 |
110 |
111 | 云时代的来临,我认为配套的构建也来到了十字路口,到底是继续维持现有的技术架构走下去,还是说另辟蹊径,寻找一条更加轻薄的方式来配合上云。
112 |
113 | ### Bundless
114 |
115 | 我们再回过来看看,2019 年为什么在社区能释放出这些声音来([Luke Jackson - Don’t Build That App!](https://formidable.com/blog/2019/no-build-step/)、[Fred K. Schott - A Future Without Webpack](https://www.pika.dev/blog/pika-web-a-future-without-webpack/)),为什么会有人敢说,我们可以有一个没有 webpack 的未来,为什么 Bundless 的想法能够成立,支撑他们这些说法的技术依据到底是什么。
116 |
117 | 归纳总结下:
118 |
119 | - 使用模块加载器,在运行时进行文件分析,从而获取依赖,完成树结构的梳理,然后对树结构开始编译
120 |
121 | 比较典型的产品有:systemjs 0.21.x & JSPM 1.x 、stackblitz 、codesandbox
122 |
123 | - 使用 Native-Module,即在浏览器中直接加载 ES-Module 的代码
124 |
125 | 比较典型的产品有:systemjs >= 3.x & JSPM 2.x 、@pika/web
126 |
127 | 再看了这些产品和技术实现后,我内心其实非常笃定,我觉得机会来了,未来肯定会是轻薄的方式来配合上云,只是这一块目前还没有人来专心突破这些点。
128 |
129 | 所以我觉得未来肯定是 `云 + Browser Based Bundless + Web NPM`,这就是 Gravity 这套技术方案出现的背景了。
130 |
131 | ### Gravity 的挑战
132 |
133 | 所有的挑战其实来源于我们从 nodejs 抽出来之后,在浏览器内的适配问题。
134 |
135 | 可以罗列下我们会碰到的问题:
136 | - nodejs 文件系统
137 | - nodejs 文件 resolve 算法
138 | - nodejs 内置模块
139 | - 任意模块格式的加载
140 | - 多媒体文件
141 | - 单一文件多种编译方式
142 | - 缓存策略
143 | - 包管理
144 | - ……
145 |
146 | 总结下其实是四个方面的问题:
147 |
148 | - 如何设计资源文件的加载器
149 | - 如何设计资源文件的编译体系
150 | - 如何设计浏览器端的文件系统
151 | - 如何设计浏览器端的包管理
152 |
153 |
154 | ## Gravity 架构大图
155 |
156 | 
157 |
158 | ### 架构图
159 |
160 | 
161 |
162 | 从这个图中其实可以归纳出,我们就是在解决上面提到四个问题,即:
163 |
164 | - 如何设计资源文件的加载器
165 | - 如何设计资源文件的编译体系
166 | - 如何设计浏览器端的文件系统
167 | - 如何设计浏览器端的包管理
168 |
169 | ### 名词解释
170 |
171 | 这里会提几个名词,方便之后大家理解。
172 |
173 | `Transpiler`: 代码 A 转换为代码 B 转换器
174 |
175 | 
176 |
177 |
178 | `Preset`: 是一份构建描述集合,该集合包含了模块加载器文件加载的描述,
179 | 转换器的描述,插件的描述等。
180 | 
181 |
182 |
183 | `Ruleset`: 具体一个文件应该被怎么样的 transpilers 来转换。
184 | 
185 |
186 |
187 | 这里可以衍生出来说一说为什么要设计 Preset 的概念。在文章的最前面我提到了现在要构建一个前端的项目学习曲线非常陡峭。在社区我们能看到两种解法:
188 |
189 | - create-react-app: 它把 react 应用开发所需要的所有细节都封装在了这个库里面,对用户只是暴露了一些基本的入口,比如启动应用,那它的好处是为着这一类 react 应用开发者提供了极致的体验,降低了整个学习曲线。但缺点也比较明显就是 CRA 并不支持自定义配置,如果你需要个性化,那不好意思,你只能 eject,一旦 eject 之后后续所有的配置就交给应用开发者,后续便不能再融入回 CRA 的闭环了。
190 | - @vue/cli: 它和 CRA 一样做了配置封装,但是和 CRA 不一样的地方是,它自身提供了一些个性化的能力,允许用户修改一些参数。
191 |
192 | 通过以上两者不难发现,他们都在做一件事情:解耦应用开发者和工具开发者。
193 |
194 | 再回到 Preset,我的角色是工具专家,提供一系列的底层能力,而 Preset 则是垂直业务专家,他们基于我的底层能力去做的业务抽象,然后把业务输出为一个 preset。而真正的应用用户其实无需感知这部分的内容,对他们而言或许只需要知道一些扩展配置。
195 |
196 |
197 | ### Gravity 的消费链
198 |
199 | 
200 |
201 | 在 Gravity的设计中,Core 层其实没有耦合任何的具体业务逻辑(这个逻辑指的是,比如 react 应用要怎么执行,vue 应用要怎么执行等),Core 层简单来讲,它是实现浏览器实时构建的事件流注册、分发、执行的集合。而具体的业务场景,比如 React,Vue,小程序等则是通过具体的 Preset 来实现整合。而我们的 Preset 会再交给对应的垂直场景的载体,比如 WebIDE 等。
202 |
203 |
204 | ## 专题深入
205 |
206 | ### 专题一: 插件机制
207 |
208 | 
209 |
210 | 事先我们来看一看 Gravity 是如何运作的,上图只是一个流程示意,但也能说明一下流程上的设计。注意看我们在 Plugin 类上定义了一些事件,而这些事件是允许被用户订阅的,那 Gravity 在执行时,会对这些事件先尝试绑定。在进入到相关的流程时,会分发这些事件,订阅了该事件的订阅者,就会在第一时间收到信息。举例来说,Plugin 中的 Code 描述了如何来获取代码的方式,而在 Gravity Core 的整个生命周期中,会调用 fetch-data 去分发 Code 事件,如果说用户订阅了该事件,那么就会马上响应去执行用户定义的获取代码的方式,并得到代码进而告诉内核。
211 |
212 | 所以不难看出,Gravity 本质上是事件流机制,它的核心流程就是将插件连接起来。
213 |
214 | 既然如此,其实我们要解决的重点就是:
215 | - 如何进行事件编排
216 | - 如何保证事件执行的有序性
217 | - 如何进行事件的订阅和消息的分发
218 |
219 | 说到这里不知道大家是不是有一种似曾听闻的感觉,没错,其实这些思路都是来自于 webpack 的设计理念,webpack 是由一堆插件来驱动的,而背后的驱动这些插件的底层能力,来源于一个名叫 [Tapable](https://github.com/webpack/tapable) 的库。
220 |
221 | [Tapable](https://github.com/webpack/tapable) 这个库我个人非常非常非常喜欢。原因在于它解决了很多我们在处理事件时会碰到的问题,比如有序性。另外要做一个插件系统的设计其实很简单,但后果是对用户会有额外的负担来学习如何书写,所以我选择 [Tapable](https://github.com/webpack/tapable) 来做还有另外很重要的一个原因,用户可以继续延续 webpack 插件写法到 Gravity 中来。
222 |
223 | 
224 |
225 | 这里我罗列一下 [Tapable](https://github.com/webpack/tapable) 所拥有的能力。并用伪代码的方式为例来讲一讲我们在核心层如何定义一个插件(定义可被订阅的事件),业务专家如何来使用这个自定义插件(订阅该事件),以及我们在核心层如何来执行这个插件(绑定,分发)。
226 |
227 | - 定义插件
228 |
229 | 
230 |
231 |
232 | - 自定义插件
233 |
234 | 
235 |
236 |
237 | - 核心层绑定和分发
238 |
239 | 
240 |
241 |
242 | 所以 `Gravity-Core 重在事件的编排和分发,Plugin 则重在事件的申明,而 Custom plugins 则是订阅这些事件来达到个性化的目的`。
243 |
244 |
245 | ### 专题二: 如何实现编译链
246 |
247 | 在讲如何实现前,我们再回过来看下 Ruleset,在架构大图小节中我说明了下,Ruleset 是用来描述一个文件应该被怎么样的 transpilers 来转换。而 Ruleset 的生成其实是依赖于 preset 中 rule 的配置,这一点,其实 Gravity 和 webpack 是一致的,这种设计原因有两点:1. 用户可以沿用 webpack 的 rule 配置习惯到 Gravity 中来;2. 我们甚至可以复用一些现有的 webpack loader,或者说让改造量变得更小。
248 |
249 | 
250 |
251 | 在这里我们以小程序中的 axml 文件为例,假设现在有一个 index.axml 需要被被编译,此时会通过 Preset 中 rule 描述,最终被拆解为一个 ruleset,在这个 set 信息中我们可以获取到 index.axml 文件需要经过怎么样的转换流程(也可以理解为该 index.axml 文件需要什么 transpiler 来进行编译)。该示例中我们可以看到,index.axml 需要经过一层 appx 小程序编译后再把对应的结果交给 babel 进行编译,而 babel 编译的结果再交给下级的消费链路。
252 |
253 | 暂时抛开复杂的业务层实现,我们想一想要实现这条串行的编译链路的本质是什么。相信大家都能找到这个答案,答案就是如何保证事件的有序性。既然又是事件,是不是我们又可以回过来看一看 [Tapable](https://github.com/webpack/tapable),没错,在 [Tapable](https://github.com/webpack/tapable) 中就有这样一个 hook - `AsyncSeriesWaterfallHook`,异步串行,上一个回调函数的返回的内容可以作为下一回调函数的参数。说到这是不是很多问题就迎刃而解了。没错,那么在 Tapable 中实现编译链是不是就被简化为如何基于 ruleset 动态创建 AsyncSeriesWaterfallHook 事件,以及如何分发的问题。
254 |
255 | ### 专题三: 文件系统和包管理
256 |
257 | ### BrowserFS
258 | 如果我们在浏览器中没有文件系统的支撑,其实可以想象本地的文件的依赖将无法被解析出来(即无法完成 resolve 过程),所以实现浏览器内的文件系统是实现浏览器编译的前提条件。这里幸运的是 [John Vilk](https://github.com/jvilk) 前辈有一个项目叫做 [BrowserFS](https://github.com/jvilk/BrowserFS),这个库在浏览器内实现了一个文件系统,同时这个文件系统模拟了 Nodejs 文件系统的 API,这样的好处就是,我们所有的 resolve 算法就可以在浏览器内实现了。同时这个库最棒的一点是提出了 backends 的概念。这个概念的背后是,我们可以自定义文件的存储和读取过程,这样文件系统的概念和思路一下子就被打开了,因为这个文件系统其实本质上并不局限于本地。
259 |
260 | 在这里我们可以大概看下如何使用 BFS。
261 |
262 | 
263 |
264 | 是不是很简单。但实际情况下大家在使用过程中,如果使用深入的话还会碰到很多问题,这些问题来自于多个文件系统间进行数据同步,会碰到不少 bug 和性能问题。这里我就不展开了。
265 |
266 |
267 | ### 包管理
268 | 有了文件系统我们再来想一想前端不可分割的一个部分,包管理。
269 |
270 | 思路一:浏览器内实现 NPM
271 |
272 | 这个思路是最容易想到的,通常做法是我们会拉取包信息,然后对包进行依赖分析,然后安装对应的包,最后把安装的包内容存储到对应的文件系统,编译器会对这些文件进行具体的编译,最后把编译结果存在文件系统里面。浏览器加执行文件时,模块加载器会加载这些编译后的文件。思路很通畅。但是这种方式的问题是原模原样照搬了 npm 到浏览器中,复杂度还是很高。
273 |
274 | 缺点:
275 | - 首次很慢
276 | - 存储量大
277 | - 依赖 NPM Scripts 的包得不到解决
278 |
279 |
280 |
281 | 思路二:服务化 NPM
282 |
283 | 这一块的思路其实来自于对我影响最大的两篇文章
284 |
285 | - [stackblitz 的 turbo CDN 思路](https://medium.com/stackblitz-blog/introducing-turbo-5x-faster-than-yarn-npm-and-runs-natively-in-browser-cc2c39715403)
286 | - [codesandbox 的 dependency-packer 思路](https://codesandbox.io/post/how-we-make-npm-packages-work-in-the-browser)
287 |
288 | 非常精彩,我也写过一些文章来分析他们。但是 stackblitz 和 codesandbox 在 npm 思路上各自都有一些缺陷,比如 stackblitz 的资源分发形式,codesandbox 的服务端缓存策略。
289 |
290 | 服务化的 NPM 本质是基于网络的本地文件系统。怎么来理解这句话呢?我们来举个例子,一起来构想一下如何基于 unpkg / jsdelivr 做一个的文件系统。
291 |
292 | 假设我们现在依赖 lodash 这个库,那么在我们对接的文件系统里面会发一个请求(https://unpkg.com/lodash@4.17.15/?meta)给远程的 unpkg,该请求可以获取到完整的目录结构(数据结构),那么在得到这份数据后,我们便可以初始化一个文件系统了,因为我们可以通过接口返回的数据完整的知道目录内会有什么,以及这个文件的尺寸,虽然没有内容。所以此时文件系统内包含了一整个完整的树结构。 假设此时我们通过 resolve 发现,我们的文件中确切依赖了一个文件是 `lodash/upperCase.js`,这个文件系统事先需要做的事情是先在本地文件数里面找下是否存在 `upperCase.js`,这里毫无疑问是存在的,因为我们在这个接口中 https://unpkg.com/lodash@4.17.15/?meta 能找到对应的 `upperCase.js` 这个文件,能确定肯定是在文件系统里面是有标记的但是如之前所说 meta 信息只是一种标记,他是没有内容的,那么接下来我们就会去往 unpkg 服务器上那固定的文件,发送请求获取该文件内容 `https://unpkg.com/lodash@4.17.15/upperCase.js`,至此我们的基于 unpkg / jsdelivr 的文件系统就设计好了。
293 |
294 |
295 | 所以服务化 NPM 的关键是:
296 |
297 | 需要我们抽象
298 | - 如何设计包管理依赖的下发逻辑
299 |
300 | 需要我们包装
301 | - 如何把这个下发逻辑桥接到对应的文件系统
302 |
303 |
304 | 注明:下发逻辑指的是我们按什么规则去下发用户的 dependencies。
305 |
306 | 服务化 NPM 的要点是:
307 |
308 | - 建立一个下发策略,比如基于项目维度的 deps,依赖的下发是基于依赖包的入口文件分析所产生的依赖文件链。
309 | - 补充在默认下发策略不满足需求时,如何建立动态下发的过程
310 | - 依赖下发的数据结构,如何体现依赖关系,父子关系等
311 | - 如何快速分析依赖关系
312 | - 如何缓存依赖关系
313 | - 如何更新缓存的依赖关系
314 | - 如何把以上这些信息桥接到我们的文件系统
315 |
316 |
317 |
318 | ## 未来
319 |
320 | 对于 Gravity 的未来,其实更多的是我对他的憧憬,总结一下可以是三个要点。
321 |
322 | ### PVC
323 |
324 | - Pipelined 流水线化
325 |
326 | 垂直业务场景所对应的 Preset 的产出,可以按着某个流程,用极少的成本自由组合一下就可以使用。
327 |
328 | - Visualized 可视化
329 |
330 | 所有搭建 Preset 、以及 Preset 内配置都可以通过可视化方式露出。
331 |
332 | - Clouds 云化
333 |
334 | Gravity 服务化。
335 |
--------------------------------------------------------------------------------
/docs/translations/_posts/2020-3-27-webpack-5-module-federation/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | sidebar: auto
3 | date: 2020-3-27
4 | author: ulivz
5 | revisor: jothy1023
6 | type: translation
7 | hero: /sddefault.jpg
8 | description: Webpack 5 新特性 —— Module Federation, 让 JavaScript 应用得以在客户端或服务器上动态运行另一个 bundle 或者 build 的代码。
9 | ---
10 |
11 | # Webpack 5 Module Federation: JavaScript 架构的变革者
12 |
13 | Module Federation [ˌfedəˈreɪʃn] 使 JavaScript 应用得以在客户端或服务器上动态运行另一个 bundle 或者 build 的代码。
14 |
15 | > 原文:[Webpack 5 Module Federation: A game-changer in JavaScript architecture](https://medium.com/@ScriptedAlchemy/webpack-5-module-federation-a-game-changer-to-javascript-architecture-bcdd30e02669)
16 |
17 | ---
18 |
19 | **Module Federation 之于 JavaScript Bundler,如同 Apollo 之于 GraphQL。**
20 |
21 | > 在独立的应用之间共享代码的可伸缩解决方案从未如此方便,而且几乎不可能大规模实现。此前我们拥有的最成熟方案是 externals 或 DLLPlugin,它们强制把依赖集中于一个外部文件中。共享代码真的很麻烦,因为很多应用看似独立,却不是真正意义上的独立,可共享的依赖非常有限。此外,在单独打包的 Web 应用之间共享实际的特性代码往往并不可行、**无效**、且毫无益处。
22 |
23 | ## 那么什么是 Module Federation?
24 |
25 | 这是我发明并且赋予它最初形态的 JavaScript 架构,在我的联合创作者兼 Webpack 创始人的帮助下,它成为了 Webpack 5 Core 中最令人兴奋的功能之一(Webpack 5 有一堆很酷的东西,新的 API 强大又干净)。
26 |
27 |
28 |
29 | 我非常荣幸地向大家介绍 JavaScript 应用架构中的一个期待已久的飞跃,我们对于开源社区的贡献:Module Federation。
30 |
31 |
32 |
33 | Module Federation 使 JavaScript 应用得以从另一个 JavaScript 应用中动态地加载代码 —— 同时共享依赖。如果某应用所消费的 federated module 没有 federated code 中所需的依赖,Webpack 将会从 federated 构建源中下载缺少的依赖项。
34 |
35 | 代码是可以共享的,但每种情况都有降级方案。federated code 可以总是加载自己的依赖,但在下载前会去尝试使用消费者的依赖。更少的代码冗余,依赖共享就像一个单一的 Webpack 构建。虽然我已经实现了这个最初的系统——他是由我自己([Zack Jackson](https://medium.com/@ScriptedAlchemy))和 [Marais Rossouw](https://medium.com/@maraisr) 在 [Tobias Koppers](https://medium.com/@sokra) 的悉心帮助和结对编程下完成的。这些开发者们在 Webpack 5 的核心重写和稳定化上起着关键性的作用,谢谢你们一直以来的合作和支持。
36 |
37 | ## 术语
38 |
39 |
40 |
41 |
42 |
43 | - **Module federation**: 与 Apollo GraphQL federation 的想法相同 —— 但适用于在浏览器或者 Node.js 中运行的 JavaScript 模块。
44 |
45 | - **host**:在页面加载过程中(当 `onLoad` 事件被触发)最先被初始化的 Webpack 构建;
46 |
47 | - **remote**:部分被 **“host”** 消费的另一个 Webpack 构建;
48 |
49 | - **Bidirectional(双向的) hosts**:当一个 bundle 或者 webpack build 作为一个 `host` 或 `remote` 运行时,它要么消费其他应用,要么被其他应用消费——均发生在运行时(runtime)。
50 |
51 | 如果你想更好地理解这篇文章,欢迎阅读[Jack Herrington](https://medium.com/@jherr) 写的文章!
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | 值得注意的是,该系统的设计使每个完全独立的构建/应用都可以存放在自己的仓库中,独立部署,并作为独立的 SPA 运行。
60 |
61 |
62 |
63 | 这些应用都是 **bidirectional(双向的) hosts**. 所有在你修改路由或者移动应用时率先加载的应用都将成为 host, 它会像你实现 dynamic imports 一样加载 federated modules. 不过,如果你要刷新页面,则任何在之后的 load 中率先启动的应用,都将成为一个 host。
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | 假设一个网站的每个页面都是独立部署和编译的,我想要这种微前端风格的架构,但不希望页面随着我更改路由而重新加载。我还希望在页面之间动态地共享代码和 vendors, 这样它就像支持 code splitting 的大型 Webpack 构建般高效了。
72 |
73 |
74 |
75 |
76 | 访问应用的 home 也将会使这个 “home” 页成为 “host”,如果你切换到 “about” 页,那么这个 host(home 页的 spa)实际上是从另一个独立应用(about 页的 spa)中动态加载一个模块,它并不会加载应用主入口以及另一个完整的应用,**而只会加载几千字节的代码**。如果我在 “about” 页刷新浏览器,“about” 页将会成为 “host”,此时我回到 “home” 页,“about” 页(“host”)将会从 “remote” 获取运行时的一些片段——这个 “remote” 就是 “home” 页。在这个系统中,所有的应用都既是 “remote” 又是 “host”,与其它 federated module 互为消费者与被消费者。
77 |
78 | 你可以在 Github 上找到更多的技术细节:
79 |
80 | [Merge Proposal: Module federation and code sharing between bundles. Many builds act as one](https://medium.com/@ScriptedAlchemy/webpack-5-module-federation-a-game-changer-to-javascript-architecture-bcdd30e02669)
81 |
82 | ## 构建一个 federated application
83 |
84 | 让我们从三个独立的应用入手。
85 |
86 | ### App One
87 |
88 | #### 配置
89 |
90 | 我将使用 **App One** 中的应用容器 ``,它将会被另一个应用消费,为此,我把 App expose 为 AppContainer,**App One** 将会同时消费来自另外两个 federated applications 中的组件,为此,我指定了 `remotes` scope:
91 |
92 | ```js
93 | const HtmlWebpackPlugin = require("html-webpack-plugin");
94 | const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
95 |
96 | module.exports = {
97 | // other webpack configs...
98 | plugins: [
99 | new ModuleFederationPlugin({
100 | name: "app_one_remote",
101 | remotes: {
102 | app_two: "app_two_remote",
103 | app_three: "app_three_remote"
104 | },
105 | exposes: {
106 | 'AppContainer':'./src/App'
107 | },
108 | shared: ["react", "react-dom","react-router-dom"]
109 | }),
110 | new HtmlWebpackPlugin({
111 | template: "./public/index.html",
112 | chunks: ["main"]
113 | })
114 | ]
115 | }
116 | ```
117 |
118 | #### 设定构建编排流程
119 |
120 | 在应用的头部,我加载了 `app_one_remote.js` —— 来让当前的应用连接到其他 Webpack runtimes,并在运行时提供一个编排层(orchestration layer),这是一个专门设计的 Webpack runtime 和 entry point,**但它不是一个普通的应用 entry point,并且只有几 KB**。
121 |
122 | **要注意,这些特殊的 entry points 大小只有几 KB——它们包含一个可以与 host 连接的特殊的 Webpack runtime,它们并非标准的 entry point。**
123 |
124 | ```html
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | ```
133 |
134 | #### 从 remote 消费代码
135 |
136 | **App One** 中有个 Page1 页面消费了 **App Two** 的 dialog 组件:
137 |
138 | ```jsx
139 | const Dialog = React.lazy(() => import("app_two_remote/Dialog"));
140 |
141 | const Page1 = () => {
142 | return (
143 |
144 |
Page 1
145 |
146 |
147 |
148 |
149 | );
150 | }
151 |
152 | export default Page1;
153 | ```
154 |
155 | 路由看起来很标准:
156 |
157 | ```jsx harmony
158 | import { Route, Switch } from "react-router-dom";
159 |
160 | import Page1 from "./pages/page1";
161 | import Page2 from "./pages/page2";
162 | import React from "react";
163 |
164 | const Routes = () => (
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 | );
174 |
175 | export default Routes;
176 | ```
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 | ### App Two
185 |
186 | #### 配置
187 |
188 |
189 | **App Two** 将会导出 Dialog,并让 **App One** 来消费它,**App Two** 将会同时消费 **App One** 的 `` —— 因此我们把 `app_one` 页设置为 “remote”。演示一下 bi-directional hosts.
190 |
191 | ```js
192 | const HtmlWebpackPlugin = require("html-webpack-plugin");
193 | const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
194 | module.exports = {
195 | plugins: [
196 | new ModuleFederationPlugin({
197 | name: "app_two_remote",
198 | library: { type: "var", name: "app_two_remote" },
199 | filename: "remoteEntry.js",
200 | exposes: {
201 | Dialog: "./src/Dialog"
202 | },
203 | remotes: {
204 | app_one: "app_one_remote",
205 | },
206 | shared: ["react", "react-dom","react-router-dom"]
207 | }),
208 | new HtmlWebpackPlugin({
209 | template: "./public/index.html",
210 | chunks: ["main"]
211 | })
212 | ]
213 | };
214 | ```
215 |
216 | #### 消费
217 |
218 | 这个是根 `App`:
219 |
220 | ```jsx harmony
221 | import React from "react";
222 | import Routes from './Routes'
223 | const AppContainer = React.lazy(() => import("app_one_remote/AppContainer"));
224 |
225 | const App = () => {
226 | return (
227 |
232 | );
233 | }
234 |
235 | export default App;
236 | ```
237 |
238 | 这个是使用了 `Dialog` 的默认页面:
239 |
240 | ```jsx harmony
241 | import React from 'react'
242 | import {ThemeProvider} from "@material-ui/core";
243 | import {theme} from "./theme";
244 | import Dialog from "./Dialog";
245 |
246 | function MainPage() {
247 | return (
248 |
249 |
250 |
Material UI App
251 |
252 |
253 |
254 | );
255 | }
256 |
257 | export default MainPage
258 | ```
259 |
260 | ### App Three
261 |
262 | 正如所料,**App Three** 看起来(和**App Two**)很相似。但是,它不消费来自 **App one** 的独立自运行的组件 `` (没有导航栏或侧边栏),因此,它并未指定任何 “remote”:
263 |
264 | ```js
265 | new ModuleFederationPlugin({
266 | name: "app_three_remote",
267 | library: { type: "var", name: "app_three_remote" },
268 | filename: "remoteEntry.js",
269 | exposes: {
270 | Button: "./src/Button"
271 | },
272 | shared: ["react", "react-dom"]
273 | }),
274 | ```
275 |
276 | ## 浏览器中的最终结果(不同于第一个视频)
277 |
278 | 请重点关注 network 标签页,来自三个不同服务器的代码正在被整合(federated),三个不同的 bundles,我们一般建议不要 federate 整个应用的容器,除非你正享受 SSR 或者渐进式加载带来的好处。不管怎么说,这个概念简直太强大了。
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 | ### 代码冗余
287 |
288 | 这里几乎没有任何依赖冗余,通过 `shared` 选项 —— **remotes 将会首先依赖来自 host 的依赖,如果 host 没有依赖,它将会下载自己的依赖。没有代码层面的冗余,而只有内置的冗余。**
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 | 然而手动地增加 vendors 或其他用于`共享`的模块并不利于拓展,你可以自定义一个函数或者相应的 Webpack 插件来实现自动化。我们的确计划发布 `AutomaticModuleFederationPlugin`,并且在 Webpack Core 之外去维护它。既然我们在 Webpack 中已经有了相当好的 code federation 支持,拓展它的功能就不值一提了。
297 |
298 | 那么大问题来了… 上面说的这些支持 SSR 吗??
299 |
300 |
301 |
302 | ## 服务端渲染
303 |
304 | 我们将其设计成了可以在任何环境下工作的 “通用模块 Federation”,服务端渲染 federated code 是完全可行的,只要让服务器构建使用一个 `commonjs` 的 library target。实现 federated SSR 有很多种办法,S3 Streaming, ESI, 自动化一个 npm 发布去消费服务器的变化内容,我计划用一个常用的共享文件的 volume 或者移步的 S3 streaming 来跨文件系统传输文件。让服务器能够像在浏览器中一样去 require federated code,使用 fs 而不是 http 来加载 federated code。
305 |
306 | ```js
307 | module.exports = {
308 | plugins: [
309 | new ModuleFederationPlugin({
310 | name: "container",
311 | library: { type: "commonjs-module" },
312 | filename: "container.js",
313 | remotes: {
314 | containerB: "../1-container-full/container.js"
315 | },
316 | shared: ["react"]
317 | })
318 | ]
319 | };
320 | ```
321 |
322 |
323 |
324 |
325 |
326 |
327 | “Module Federation 当然也适用于 `target: "node"`,这里使用指向其他微前端应用的文件路径,而不是 URLs。这样的话你就可以用同样的代码,外加不一样的 Webpack 配置来在 Node.js 中实现 SSR. Module Federation 的特性在 Node.js 中保持不变,如独立构建、独立部署。—— Tobias Koppers
328 |
329 |
330 |
331 | ## Federated Next.js on Webpack 5
332 |
333 | Federation 依赖 Webpack 5 —— Next 并未官方支持。不过我已经 fork Next.js,并设法升级它以支持 Webpack 5 了!这项工作仍在进行中,还有一些开发模式下的中间件需要完成。生产模式已经搞定了,但某些附加的 loaders 仍然需要重新测试。
334 |
335 | [feat: Upgrade Next.js to Webpack 5](https://github.com/module-federation/next.js/pull/2)
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 | ### Talks, podcasts, or feedback
344 |
345 |
346 | 我非常想有和大家分享这项技术的机会,如果你想要使用 `Module Federation` 或者 `Federated architecture`,我们想要听到你的使用经历以及对现有的架构的调整。我们也非常期望能够有机会在 Podcasts、meetups 或者 corporations 中讨论它。你可以通过 [Twitter](https://twitter.com/ScriptedAlchemy) 联系到我。
347 |
348 |
349 |
350 | [](https://twitter.com/ScriptedAlchemy)
351 |
352 |
353 |
354 |
355 | 你还可以联系我的联合创作者,来了解 Module Federation 的最新变化、FOSA (Federation of Standalone Applications) 架构,以及我们正在做的其他工具 —— 用于和 Federated Applications 共用的工具。
356 |
357 |
358 |
359 | [](https://twitter.com/codervandal)
360 |
361 |
362 |
363 |
364 | ## 使用 Module Federation 的例子
365 |
366 | 社区对 Module Federation 的反馈非常热烈!我和我的创作者们的大部分时间,都集中在将这项特性写到 Webpack 5 中,当我们忙于完成剩余特性,以及书写文档时,希望这些代码示例能对你有帮助。
367 |
368 |
369 |
370 | [](https://dev.to/marais/webpack-5-and-module-federation-4j1i)
371 |
372 |
373 |
374 | 因为我们有带宽,所以我们将创建一些 SSR 示例和更全面的 demo,如果有人想建设可以作为 demo 使用的东西 —— 我们很乐意接受提交给 `webpack-external-import` 的 pull requests。
375 |
376 |
377 |
378 | [](https://github.com/module-federation/module-federation-examples)
379 |
380 |
381 |
382 |
383 |
384 | [](https://github.com/module-federation/module-federation-examples)
385 |
386 |
387 |
388 |
389 |
390 |
391 | [](https://github.com/ScriptedAlchemy/webpack-external-import)
392 |
393 |
394 |
395 | 感谢 Tobias Koppers.
396 |
--------------------------------------------------------------------------------