├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── LICENSE ├── README.md ├── deploy.sh ├── docs ├── .vuepress │ ├── config.js │ ├── enhanceApp.js │ └── theme │ │ ├── LICENSE │ │ ├── components │ │ ├── AlgoliaSearchBox.vue │ │ ├── CopyRight.vue │ │ ├── DropdownLink.vue │ │ ├── DropdownTransition.vue │ │ ├── Home.vue │ │ ├── Kitty.vue │ │ ├── KittySleep.vue │ │ ├── KittyTop.vue │ │ ├── NavLink.vue │ │ ├── NavLinks.vue │ │ ├── Navbar.vue │ │ ├── Page.vue │ │ ├── Sidebar.vue │ │ ├── SidebarButton.vue │ │ ├── SidebarGroup.vue │ │ ├── SidebarLink.vue │ │ └── SidebarLinks.vue │ │ ├── global-components │ │ └── Badge.vue │ │ ├── index.js │ │ ├── layouts │ │ ├── 404.vue │ │ └── Layout.vue │ │ ├── noopModule.js │ │ ├── styles │ │ ├── arrow.styl │ │ ├── code.styl │ │ ├── config.styl │ │ ├── custom-blocks.styl │ │ ├── index.styl │ │ ├── mobile.styl │ │ ├── toc.styl │ │ └── wrapper.styl │ │ └── util │ │ └── index.js ├── README.md └── vue-ebook │ ├── 0.md │ ├── 1.md │ ├── 10.md │ ├── 11.md │ ├── 12.md │ ├── 13.md │ ├── 14.md │ ├── 15.md │ ├── 16.md │ ├── 2.md │ ├── 3.md │ ├── 4.md │ ├── 5.md │ ├── 6.md │ ├── 7.md │ ├── 8.md │ ├── 9.md │ ├── 99.md │ └── README.md └── package.json /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy site files 2 | 3 | on: 4 | push: 5 | branches: 6 | - ebook-sourcecode # 只在master上push触发部署 7 | paths-ignore: # 下列文件的变更不触发部署,可以自行添加 8 | - README.md 9 | - LICENSE 10 | pull_request: 11 | branches: 12 | - ebook-sourcecode # 只在master上push触发部署 13 | types: [closed] 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | node-version: [10.x] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | with: 25 | repo-token: ${{ secrets.DEPLOY_TOKEN }} 26 | 27 | - name: Use Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | 32 | - name: Install dependencies 33 | run: | 34 | npm i 35 | - name: Build 36 | run: | 37 | npm run build 38 | cp README.md docs/.vuepress/dist/ 39 | cd docs/.vuepress/dist 40 | - name: Deploy 41 | uses: JamesIves/github-pages-deploy-action@3.6.2 42 | with: 43 | GITHUB_TOKEN: ${{ secrets.DEPLOY_TOKEN }} 44 | BRANCH: master # The branch the action should deploy to. 45 | FOLDER: docs/.vuepress/dist/ 46 | CLEAN: true 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | package-lock.json 3 | docs/.vuepress/dist/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Creative Commons Attribution-NonCommercial 4.0 International License 2 | 3 | Disclaimer: This is a human-readable summary of (and not a substitute for) the [license](http://creativecommons.org/licenses/by-nc/4.0/legalcode). 4 | 5 | You are free to: 6 | 7 | - Share — copy and redistribute the material in any medium or format 8 | - Adapt — remix, transform, and build upon the material 9 | 10 | The licensor cannot revoke these freedoms as long as you follow the license terms. 11 | 12 | Under the following terms: 13 | 14 | - Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. 15 | - NonCommercial — You may not use the material for commercial purposes. 16 | - No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. 17 | 18 | Notices: 19 | 20 | You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation. 21 | 22 | No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 深入理解 Vue.js 实战 2 | 3 | 本书为开源的 Vue.js 教程,全面介绍 Vue.js 框架的出现、设计和使用,结合实战让读者更深入理解 Vue.js 框架,掌握使用方法。 4 | 5 | - 本书内容基于 Vue.js 2.x 版本 6 | - [本书地址](https://godbasin.github.io/vue-ebook/) 7 | - [如果Github Pages访问不了,可以访问这里](http://www.godbasin.com/vue-ebook/) 8 | - [本书Github](https://github.com/godbasin/vue-ebook/) 9 | - 这本书已上架图灵电子书,欢迎阅读: [https://www.ituring.com.cn/book/2941](https://www.ituring.com.cn/book/2941) 10 | 11 | ## 结构和内容 12 | 13 | 《深入理解Vue.js实战》一书主要介绍了这些年来使用Vue.js框架、前端开发的一些理解和经验积累,可以结合官方教程一起阅读。本书共分为两部分。 14 | 15 | ### 第一部分 Vue快速入门 16 | 17 | 该部分介绍Vue快速入门,结合Vue基础、Vue框架与设计,以及一些简单的代码实例来快速入门。包括8章内容: 18 | - [第1章 Vue 框架介绍](https://godbasin.github.io/vue-ebook/vue-ebook/1.html) 19 | - [第2章 Vue 环境快速搭建](https://godbasin.github.io/vue-ebook/vue-ebook/2.html) 20 | - [第3章 Vue 基础介绍](https://godbasin.github.io/vue-ebook/vue-ebook/3.html) 21 | - [第4章 Vue 组件的使用](https://godbasin.github.io/vue-ebook/vue-ebook/4.html) 22 | - [第5章 常用指令和自定义指令](https://godbasin.github.io/vue-ebook/vue-ebook/5.html) 23 | - [第6章 Vue 动画](https://godbasin.github.io/vue-ebook/vue-ebook/6.html) 24 | - [第7章 Vue Router 路由搭建应用](https://godbasin.github.io/vue-ebook/vue-ebook/7.html) 25 | - [第8章 实战:Todo List 从组件到应用](https://godbasin.github.io/vue-ebook/vue-ebook/8.html) 26 | 27 | ### 第二部分 Vue的正确使用方式 28 | 29 | 该部分介绍Vue的正确使用方式,包括在日常开发中,常见的使用方式和项目实战。同样包括8章内容: 30 | - [第9章 思维转变与大型项目管理](https://godbasin.github.io/vue-ebook/vue-ebook/9.html) 31 | - [第10章 如何正确地进行抽象](https://godbasin.github.io/vue-ebook/vue-ebook/10.html) 32 | - [第11章 全局数据管理与 Vuex](https://godbasin.github.io/vue-ebook/vue-ebook/11.html) 33 | - [第12章 实战:三天开发一个管理端](https://godbasin.github.io/vue-ebook/vue-ebook/12.html) 34 | - [第13章 实战:表单配置化实现](https://godbasin.github.io/vue-ebook/vue-ebook/13.html) 35 | - [第14章 实战:使用 Webpack 或 Vue CLI 搭建多页应用](https://godbasin.github.io/vue-ebook/vue-ebook/14.html) 36 | - [第15章 Vue 周边拓展](https://godbasin.github.io/vue-ebook/vue-ebook/15.html) 37 | - [第16章 关于 Vue 3.0](https://godbasin.github.io/vue-ebook/vue-ebook/16.html) 38 | 39 | > 本书相关代码存放在[vue-sourcecode 分支](https://github.com/godbasin/vue-ebook/tree/vue-sourcecode),书中涉及的代码示例基本可以在这里找到。 40 | 41 | 除此之外,本书还介绍了: 42 | - 前端框架的出现,理解Vue.js框架原理 43 | - 使用Vue.js解决大型项目的实践经验 44 | - 如何高效、灵活地开发Vue.js应用 45 | - Vue.js相关拓展工具的使用 46 | - Vue3.0的设计和源码理解 47 | - 前端项目选型要如何进行决策 48 | 49 | ## 本书说明 50 | 51 | 该书的出现源于去年帮助团队的成员快速入门 Vue.js,在写完《9102 全员学 Vue》系列文章后,对于如何系统学习和理解 Vue.js、日常的项目管理和实践又是如何进行,依然有很多可以传授的内容。 52 | 53 | 而将这些内容整理下来,开源给到更多有需要的人便是我的本意。本书的完成特别感谢图灵出版社的张霞编辑,在她的帮助下该书有了完整的写作方向,同时十分感谢张霞编辑支持本人最终进行开源的选择。 54 | 55 | ## 版权许可 56 | 57 | 本书采用“保持署名—非商用”创意共享 4.0 许可证。只要保持原作者署名和非商用,您可以自由地阅读、分享、修改本书。 58 | 详细的法律条文请参见[创意共享](http://creativecommons.org/licenses/by-nc/4.0/)网站。 59 | 60 | ## 赞赏码 61 | 62 | **写文不易,欢迎各种花式赞赏:** 63 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/2code2.jpg) 64 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 确保脚本抛出遇到的错误 4 | set -e 5 | 6 | # 生成静态文件 7 | npm run docs:build 8 | 9 | cp README.md docs/.vuepress/dist/ 10 | 11 | # 进入生成的文件夹 12 | cd docs/.vuepress/dist 13 | 14 | # 如果是发布到自定义域名 15 | # echo 'www.example.com' > CNAME 16 | 17 | git init 18 | git add -A 19 | git commit -m 'deploy' 20 | git remote add origin https://github.com/godbasin/vue-ebook.git 21 | git push origin master -f 22 | 23 | # 如果发布到 https://.github.io 24 | # git push -f git@github.com:/.github.io.git master 25 | 26 | # 如果发布到 https://.github.io/ 27 | # git push -f godbasin@github.com:https://github.com/godbasin/front-end-playground.git master:gh-pages 28 | 29 | cd - -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: '深入理解Vue.js实战', 3 | base: '/vue-ebook/', 4 | shouldPrefetch: () => false, 5 | description: '作者:被删', 6 | themeConfig: { 7 | // logo: '/assets/img/logo.png', 8 | // 假定是 GitHub. 同时也可以是一个完整的 GitLab URL 9 | repo: 'godbasin/vue-ebook', 10 | // 自定义仓库链接文字。默认从 `themeConfig.repo` 中自动推断为 11 | // "GitHub"/"GitLab"/"Bitbucket" 其中之一,或是 "Source"。 12 | repoLabel: 'Github', 13 | 14 | // 以下为可选的编辑链接选项 15 | 16 | // 假如你的文档仓库和项目本身不在一个仓库: 17 | // docsRepo: 'vuejs/vuepress', 18 | // 假如文档不是放在仓库的根目录下: 19 | docsDir: 'docs', 20 | // 假如文档放在一个特定的分支下: 21 | docsBranch: 'ebook-sourcecode', 22 | // 默认是 false, 设置为 true 来启用 23 | editLinks: true, 24 | // 默认为 "Edit this page" 25 | editLinkText: '帮阿猪改善此页面!', 26 | nav: [ 27 | { text: '概述', link: '/' }, 28 | { text: '内容', link: '/vue-ebook/' }, 29 | ], 30 | sidebar: { 31 | '/vue-ebook/': [ 32 | { 33 | title: '前言', 34 | collapsable: false, 35 | children: [ 36 | '/vue-ebook/0.md' 37 | ] 38 | }, { 39 | title: '第一部分 Vue快速入门', 40 | collapsable: false, 41 | children: [ 42 | '/vue-ebook/1.md', 43 | '/vue-ebook/2.md', 44 | '/vue-ebook/3.md', 45 | '/vue-ebook/4.md', 46 | '/vue-ebook/5.md', 47 | '/vue-ebook/6.md', 48 | '/vue-ebook/7.md', 49 | '/vue-ebook/8.md', 50 | ] 51 | }, { 52 | title: '第二部分 Vue的正确使用方式', 53 | collapsable: false, 54 | children: [ 55 | '/vue-ebook/9.md', 56 | '/vue-ebook/10.md', 57 | '/vue-ebook/11.md', 58 | '/vue-ebook/12.md', 59 | '/vue-ebook/13.md', 60 | '/vue-ebook/14.md', 61 | '/vue-ebook/15.md', 62 | '/vue-ebook/16.md', 63 | ] 64 | }, { 65 | title: '后记', 66 | collapsable: false, 67 | children: [ 68 | '/vue-ebook/99.md', 69 | ] 70 | } 71 | ], 72 | } 73 | }, 74 | 75 | }; -------------------------------------------------------------------------------- /docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | function integrateGitalk(router) { 2 | try{ 3 | const linkGitalk = document.createElement('link'); 4 | linkGitalk.href = 'https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/gitalk.css'; 5 | linkGitalk.rel = 'stylesheet'; 6 | document.body.appendChild(linkGitalk); 7 | const scriptGitalk = document.createElement('script'); 8 | scriptGitalk.src = 'https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/gitalk.min.js'; 9 | document.body.appendChild(scriptGitalk); 10 | 11 | router.afterEach((to, from) => { 12 | // 页面滚动,hash值变化,也会触发afterEach钩子,避免重新渲染 13 | if (to.path === from.path) return 14 | 15 | if (scriptGitalk.onload) { 16 | loadGitalk(to); 17 | } else { 18 | scriptGitalk.onload = () => { 19 | loadGitalk(to); 20 | } 21 | } 22 | }); 23 | }catch(e){ 24 | console.log(e) 25 | } 26 | 27 | function loadGitalk(to) { 28 | let commentsContainer = document.getElementById('gitalk-container'); 29 | if (!commentsContainer) { 30 | commentsContainer = document.createElement('div'); 31 | commentsContainer.id = 'gitalk-container'; 32 | commentsContainer.classList.add('content'); 33 | } 34 | const $page = document.querySelector('.gitalk-container'); 35 | if ($page) { 36 | $page.appendChild(commentsContainer); 37 | if (typeof Gitalk !== 'undefined' && Gitalk instanceof Function) { 38 | renderGitalk(to.path); 39 | } 40 | } 41 | } 42 | function renderGitalk(path) { 43 | const gitalk = new Gitalk({ 44 | clientID: '1e0f6e251a3692a7e176', 45 | clientSecret: '37c9121a50e293aa5712ac9814fdfd2c775b6274', // come from github development 46 | repo: 'front-end-playground', 47 | owner: 'godbasin', 48 | admin: ['godbasin'], 49 | id: path, 50 | distractionFreeMode: false, 51 | language: 'zh-CN', 52 | }); 53 | gitalk.render('gitalk-container'); 54 | } 55 | } 56 | 57 | import ElementUI from "element-ui"; 58 | import "element-ui/lib/theme-chalk/index.css"; 59 | 60 | export default ({ 61 | Vue, // VuePress 正在使用的 Vue 构造函数 62 | options, // 附加到根实例的一些选项 63 | router, // 当前应用的路由实例 64 | siteData // 站点元数据 65 | }) => { 66 | try { 67 | // 使用element-ui 68 | Vue.use(ElementUI); 69 | 70 | // document && integrateGitalk(router); 71 | } catch (e) { 72 | console.error(e.message); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present, Yuxi (Evan) You 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/AlgoliaSearchBox.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 61 | 62 | 156 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/CopyRight.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 55 | 56 | 81 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/DropdownLink.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 78 | 79 | 180 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/DropdownTransition.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/Home.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 72 | 73 | 163 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/Kitty.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 181 | 182 | 261 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/KittySleep.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 43 | 44 | 393 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/KittyTop.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | 21 | 123 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/NavLink.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 50 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/NavLinks.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 117 | 118 | 150 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 87 | 88 | 129 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/Page.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 205 | 206 | 256 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 60 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/SidebarButton.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 28 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/SidebarGroup.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 74 | 75 | 134 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/SidebarLink.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 125 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/components/SidebarLinks.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 87 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/global-components/Badge.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 45 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | // Theme API. 4 | module.exports = (options, ctx) => ({ 5 | alias () { 6 | const { themeConfig, siteConfig } = ctx 7 | // resolve algolia 8 | const isAlgoliaSearch = ( 9 | themeConfig.algolia 10 | || Object.keys(siteConfig.locales && themeConfig.locales || {}) 11 | .some(base => themeConfig.locales[base].algolia) 12 | ) 13 | return { 14 | '@AlgoliaSearchBox': isAlgoliaSearch 15 | ? path.resolve(__dirname, 'components/AlgoliaSearchBox.vue') 16 | : path.resolve(__dirname, 'noopModule.js') 17 | } 18 | }, 19 | 20 | plugins: [ 21 | ['@vuepress/active-header-links', options.activeHeaderLinks], 22 | '@vuepress/search', 23 | '@vuepress/plugin-nprogress', 24 | ['container', { 25 | type: 'tip', 26 | defaultTitle: { 27 | '/zh/': '提示' 28 | } 29 | }], 30 | ['container', { 31 | type: 'warning', 32 | defaultTitle: { 33 | '/zh/': '注意' 34 | } 35 | }], 36 | ['container', { 37 | type: 'danger', 38 | defaultTitle: { 39 | '/zh/': '警告' 40 | } 41 | }] 42 | ] 43 | }) 44 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/layouts/404.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/layouts/Layout.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/noopModule.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/arrow.styl: -------------------------------------------------------------------------------- 1 | @require './config' 2 | 3 | .arrow 4 | display inline-block 5 | width 0 6 | height 0 7 | &.up 8 | border-left 4px solid transparent 9 | border-right 4px solid transparent 10 | border-bottom 6px solid $arrowBgColor 11 | &.down 12 | border-left 4px solid transparent 13 | border-right 4px solid transparent 14 | border-top 6px solid $arrowBgColor 15 | &.right 16 | border-top 4px solid transparent 17 | border-bottom 4px solid transparent 18 | border-left 6px solid $arrowBgColor 19 | &.left 20 | border-top 4px solid transparent 21 | border-bottom 4px solid transparent 22 | border-right 6px solid $arrowBgColor 23 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/code.styl: -------------------------------------------------------------------------------- 1 | {$contentClass} 2 | code 3 | color lighten($textColor, 20%) 4 | padding 0.25rem 0.5rem 5 | margin 0 6 | font-size 0.85em 7 | background-color rgba(27,31,35,0.05) 8 | border-radius 3px 9 | .token 10 | &.deleted 11 | color #EC5975 12 | &.inserted 13 | color $accentColor 14 | 15 | {$contentClass} 16 | pre, pre[class*="language-"] 17 | line-height 1.4 18 | padding 1.25rem 1.5rem 19 | margin 0.85rem 0 20 | background-color $codeBgColor 21 | border-radius 6px 22 | overflow auto 23 | code 24 | color #fff 25 | padding 0 26 | background-color transparent 27 | border-radius 0 28 | 29 | div[class*="language-"] 30 | position relative 31 | background-color $codeBgColor 32 | border-radius 6px 33 | .highlight-lines 34 | user-select none 35 | padding-top 1.3rem 36 | position absolute 37 | top 0 38 | left 0 39 | width 100% 40 | line-height 1.4 41 | .highlighted 42 | background-color rgba(0, 0, 0, 66%) 43 | pre, pre[class*="language-"] 44 | background transparent 45 | position relative 46 | z-index 1 47 | &::before 48 | position absolute 49 | z-index 3 50 | top 0.8em 51 | right 1em 52 | font-size 0.75rem 53 | color rgba(255, 255, 255, 0.4) 54 | &:not(.line-numbers-mode) 55 | .line-numbers-wrapper 56 | display none 57 | &.line-numbers-mode 58 | .highlight-lines .highlighted 59 | position relative 60 | &:before 61 | content ' ' 62 | position absolute 63 | z-index 3 64 | left 0 65 | top 0 66 | display block 67 | width $lineNumbersWrapperWidth 68 | height 100% 69 | background-color rgba(0, 0, 0, 66%) 70 | pre 71 | padding-left $lineNumbersWrapperWidth + 1 rem 72 | vertical-align middle 73 | .line-numbers-wrapper 74 | position absolute 75 | top 0 76 | width $lineNumbersWrapperWidth 77 | text-align center 78 | color rgba(255, 255, 255, 0.3) 79 | padding 1.25rem 0 80 | line-height 1.4 81 | br 82 | user-select none 83 | .line-number 84 | position relative 85 | z-index 4 86 | user-select none 87 | font-size 0.85em 88 | &::after 89 | content '' 90 | position absolute 91 | z-index 2 92 | top 0 93 | left 0 94 | width $lineNumbersWrapperWidth 95 | height 100% 96 | border-radius 6px 0 0 6px 97 | border-right 1px solid rgba(0, 0, 0, 66%) 98 | background-color $codeBgColor 99 | 100 | 101 | for lang in $codeLang 102 | div{'[class~="language-' + lang + '"]'} 103 | &:before 104 | content ('' + lang) 105 | 106 | div[class~="language-javascript"] 107 | &:before 108 | content "js" 109 | 110 | div[class~="language-typescript"] 111 | &:before 112 | content "ts" 113 | 114 | div[class~="language-markup"] 115 | &:before 116 | content "html" 117 | 118 | div[class~="language-markdown"] 119 | &:before 120 | content "md" 121 | 122 | div[class~="language-json"]:before 123 | content "json" 124 | 125 | div[class~="language-ruby"]:before 126 | content "rb" 127 | 128 | div[class~="language-python"]:before 129 | content "py" 130 | 131 | div[class~="language-bash"]:before 132 | content "sh" 133 | 134 | div[class~="language-php"]:before 135 | content "php" 136 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/config.styl: -------------------------------------------------------------------------------- 1 | $contentClass = '.theme-default-content' 2 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/custom-blocks.styl: -------------------------------------------------------------------------------- 1 | .custom-block 2 | .custom-block-title 3 | font-weight 600 4 | margin-bottom -0.4rem 5 | &.tip, &.warning, &.danger 6 | padding .1rem 1.5rem 7 | border-left-width .5rem 8 | border-left-style solid 9 | margin 1rem 0 10 | &.tip 11 | background-color #f3f5f7 12 | border-color #42b983 13 | &.warning 14 | background-color rgba(255,229,100,.3) 15 | border-color darken(#ffe564, 35%) 16 | color darken(#ffe564, 70%) 17 | .custom-block-title 18 | color darken(#ffe564, 50%) 19 | a 20 | color $textColor 21 | &.danger 22 | background-color #ffe6e6 23 | border-color darken(red, 20%) 24 | color darken(red, 70%) 25 | .custom-block-title 26 | color darken(red, 40%) 27 | a 28 | color $textColor 29 | 30 | 31 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/index.styl: -------------------------------------------------------------------------------- 1 | @require './config' 2 | @require './code' 3 | @require './custom-blocks' 4 | @require './arrow' 5 | @require './wrapper' 6 | @require './toc' 7 | 8 | html, body 9 | padding 0 10 | margin 0 11 | background-color #fff 12 | 13 | body 14 | font-family -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif 15 | -webkit-font-smoothing antialiased 16 | -moz-osx-font-smoothing grayscale 17 | font-size 16px 18 | color $textColor 19 | 20 | .page 21 | padding-left $sidebarWidth 22 | 23 | .navbar 24 | position fixed 25 | z-index 20 26 | top 0 27 | left 0 28 | right 0 29 | height $navbarHeight 30 | background-color #fff 31 | box-sizing border-box 32 | border-bottom 1px solid $borderColor 33 | 34 | .sidebar-mask 35 | position fixed 36 | z-index 9 37 | top 0 38 | left 0 39 | width 100vw 40 | height 100vh 41 | display none 42 | 43 | .sidebar 44 | font-size 16px 45 | background-color #fff 46 | width $sidebarWidth 47 | position fixed 48 | z-index 10 49 | margin 0 50 | top $navbarHeight 51 | left 0 52 | bottom 0 53 | box-sizing border-box 54 | border-right 1px solid $borderColor 55 | overflow-y auto 56 | 57 | {$contentClass}:not(.custom) 58 | @extend $wrapper 59 | > *:first-child 60 | margin-top $navbarHeight 61 | 62 | a:hover 63 | text-decoration underline 64 | 65 | p.demo 66 | padding 1rem 1.5rem 67 | border 1px solid #ddd 68 | border-radius 4px 69 | 70 | img 71 | max-width 100% 72 | 73 | {$contentClass}.custom 74 | padding 0 75 | margin 0 76 | 77 | img 78 | max-width 100% 79 | 80 | a 81 | font-weight 500 82 | color $accentColor 83 | text-decoration none 84 | 85 | p a code 86 | font-weight 400 87 | color $accentColor 88 | 89 | kbd 90 | background #eee 91 | border solid 0.15rem #ddd 92 | border-bottom solid 0.25rem #ddd 93 | border-radius 0.15rem 94 | padding 0 0.15em 95 | 96 | blockquote 97 | font-size 1rem 98 | color #999; 99 | border-left .2rem solid #dfe2e5 100 | margin 1rem 0 101 | padding .25rem 0 .25rem 1rem 102 | 103 | & > p 104 | margin 0 105 | 106 | ul, ol 107 | padding-left 1.2em 108 | 109 | strong 110 | font-weight 600 111 | 112 | h1, h2, h3, h4, h5, h6 113 | font-weight 600 114 | line-height 1.25 115 | 116 | {$contentClass}:not(.custom) > & 117 | margin-top (0.5rem - $navbarHeight) 118 | padding-top ($navbarHeight + 1rem) 119 | margin-bottom 0 120 | 121 | &:first-child 122 | margin-top -1.5rem 123 | margin-bottom 1rem 124 | 125 | + p, + pre, + .custom-block 126 | margin-top 2rem 127 | 128 | &:hover .header-anchor 129 | opacity: 1 130 | 131 | h1 132 | font-size 2.2rem 133 | 134 | h2 135 | font-size 1.65rem 136 | padding-bottom .3rem 137 | border-bottom 1px solid $borderColor 138 | 139 | h3 140 | font-size 1.35rem 141 | 142 | a.header-anchor 143 | font-size 0.85em 144 | float left 145 | margin-left -0.87em 146 | padding-right 0.23em 147 | margin-top 0.125em 148 | opacity 0 149 | 150 | &:hover 151 | text-decoration none 152 | 153 | code, kbd, .line-number 154 | font-family source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace 155 | 156 | p, ul, ol 157 | line-height 1.7 158 | 159 | hr 160 | border 0 161 | border-top 1px solid $borderColor 162 | 163 | table 164 | border-collapse collapse 165 | margin 1rem 0 166 | display: block 167 | overflow-x: auto 168 | 169 | tr 170 | border-top 1px solid #dfe2e5 171 | 172 | &:nth-child(2n) 173 | background-color #f6f8fa 174 | 175 | th, td 176 | border 1px solid #dfe2e5 177 | padding .6em 1em 178 | 179 | .theme-container 180 | &.sidebar-open 181 | .sidebar-mask 182 | display: block 183 | 184 | &.no-navbar 185 | {$contentClass}:not(.custom) > h1, h2, h3, h4, h5, h6 186 | margin-top 1.5rem 187 | padding-top 0 188 | 189 | .sidebar 190 | top 0 191 | 192 | 193 | @media (min-width: ($MQMobile + 1px)) 194 | .theme-container.no-sidebar 195 | .sidebar 196 | display none 197 | 198 | .page 199 | padding-left 0 200 | 201 | @require 'mobile.styl' 202 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/mobile.styl: -------------------------------------------------------------------------------- 1 | @require './config' 2 | 3 | $mobileSidebarWidth = $sidebarWidth * 0.82 4 | 5 | // narrow desktop / iPad 6 | @media (max-width: $MQNarrow) 7 | .sidebar 8 | font-size 15px 9 | width $mobileSidebarWidth 10 | .page 11 | padding-left $mobileSidebarWidth 12 | 13 | // wide mobile 14 | @media (max-width: $MQMobile) 15 | .sidebar 16 | top 0 17 | padding-top $navbarHeight 18 | transform translateX(-100%) 19 | transition transform .2s ease 20 | .page 21 | padding-left 0 22 | .theme-container 23 | &.sidebar-open 24 | .sidebar 25 | transform translateX(0) 26 | &.no-navbar 27 | .sidebar 28 | padding-top: 0 29 | 30 | // narrow mobile 31 | @media (max-width: $MQMobileNarrow) 32 | h1 33 | font-size 1.9rem 34 | {$contentClass} 35 | div[class*="language-"] 36 | margin 0.85rem -1.5rem 37 | border-radius 0 38 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/toc.styl: -------------------------------------------------------------------------------- 1 | .table-of-contents 2 | .badge 3 | vertical-align middle 4 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/styles/wrapper.styl: -------------------------------------------------------------------------------- 1 | $wrapper 2 | max-width $contentWidth 3 | margin 0 auto 4 | padding 2rem 2.5rem 5 | @media (max-width: $MQNarrow) 6 | padding 2rem 7 | @media (max-width: $MQMobileNarrow) 8 | padding 1.5rem 9 | 10 | -------------------------------------------------------------------------------- /docs/.vuepress/theme/util/index.js: -------------------------------------------------------------------------------- 1 | export const hashRE = /#.*$/ 2 | export const extRE = /\.(md|html)$/ 3 | export const endingSlashRE = /\/$/ 4 | export const outboundRE = /^(https?:|mailto:|tel:)/ 5 | 6 | export function normalize (path) { 7 | return decodeURI(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 isTel (path) { 28 | return /^tel:/.test(path) 29 | } 30 | 31 | export function ensureExt (path) { 32 | if (isExternal(path)) { 33 | return path 34 | } 35 | const hashMatch = path.match(hashRE) 36 | const hash = hashMatch ? hashMatch[0] : '' 37 | const normalized = normalize(path) 38 | 39 | if (endingSlashRE.test(normalized)) { 40 | return path 41 | } 42 | return normalized + '.html' + hash 43 | } 44 | 45 | export function isActive (route, path) { 46 | const routeHash = route.hash 47 | const linkHash = getHash(path) 48 | if (linkHash && routeHash !== linkHash) { 49 | return false 50 | } 51 | const routePath = normalize(route.path) 52 | const pagePath = normalize(path) 53 | return routePath === pagePath 54 | } 55 | 56 | export function resolvePage (pages, rawPath, base) { 57 | if (isExternal(rawPath)) { 58 | return { 59 | type: 'external', 60 | path: rawPath 61 | } 62 | } 63 | if (base) { 64 | rawPath = resolvePath(rawPath, base) 65 | } 66 | const path = normalize(rawPath) 67 | for (let i = 0; i < pages.length; i++) { 68 | if (normalize(pages[i].regularPath) === path) { 69 | return Object.assign({}, pages[i], { 70 | type: 'page', 71 | path: ensureExt(pages[i].path) 72 | }) 73 | } 74 | } 75 | console.error(`[vuepress] No matching page found for sidebar item "${rawPath}"`) 76 | return {} 77 | } 78 | 79 | function resolvePath (relative, base, append) { 80 | const firstChar = relative.charAt(0) 81 | if (firstChar === '/') { 82 | return relative 83 | } 84 | 85 | if (firstChar === '?' || firstChar === '#') { 86 | return base + relative 87 | } 88 | 89 | const stack = base.split('/') 90 | 91 | // remove trailing segment if: 92 | // - not appending 93 | // - appending to trailing slash (last segment is empty) 94 | if (!append || !stack[stack.length - 1]) { 95 | stack.pop() 96 | } 97 | 98 | // resolve relative path 99 | const segments = relative.replace(/^\//, '').split('/') 100 | for (let i = 0; i < segments.length; i++) { 101 | const segment = segments[i] 102 | if (segment === '..') { 103 | stack.pop() 104 | } else if (segment !== '.') { 105 | stack.push(segment) 106 | } 107 | } 108 | 109 | // ensure leading slash 110 | if (stack[0] !== '') { 111 | stack.unshift('') 112 | } 113 | 114 | return stack.join('/') 115 | } 116 | 117 | /** 118 | * @param { Page } page 119 | * @param { string } regularPath 120 | * @param { SiteData } site 121 | * @param { string } localePath 122 | * @returns { SidebarGroup } 123 | */ 124 | export function resolveSidebarItems (page, regularPath, site, localePath) { 125 | const { pages, themeConfig } = site 126 | 127 | const localeConfig = localePath && themeConfig.locales 128 | ? themeConfig.locales[localePath] || themeConfig 129 | : themeConfig 130 | 131 | const pageSidebarConfig = page.frontmatter.sidebar || localeConfig.sidebar || themeConfig.sidebar 132 | if (pageSidebarConfig === 'auto') { 133 | return resolveHeaders(page) 134 | } 135 | 136 | const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar 137 | if (!sidebarConfig) { 138 | return [] 139 | } else { 140 | const { base, config } = resolveMatchingConfig(regularPath, sidebarConfig) 141 | return config 142 | ? config.map(item => resolveItem(item, pages, base)) 143 | : [] 144 | } 145 | } 146 | 147 | /** 148 | * @param { Page } page 149 | * @returns { SidebarGroup } 150 | */ 151 | function resolveHeaders (page) { 152 | const headers = groupHeaders(page.headers || []) 153 | return [{ 154 | type: 'group', 155 | collapsable: false, 156 | title: page.title, 157 | path: null, 158 | children: headers.map(h => ({ 159 | type: 'auto', 160 | title: h.title, 161 | basePath: page.path, 162 | path: page.path + '#' + h.slug, 163 | children: h.children || [] 164 | })) 165 | }] 166 | } 167 | 168 | export function groupHeaders (headers) { 169 | // group h3s under h2 170 | headers = headers.map(h => Object.assign({}, h)) 171 | let lastH2 172 | headers.forEach(h => { 173 | if (h.level === 2) { 174 | lastH2 = h 175 | } else if (lastH2) { 176 | (lastH2.children || (lastH2.children = [])).push(h) 177 | } 178 | }) 179 | return headers.filter(h => h.level === 2) 180 | } 181 | 182 | export function resolveNavLinkItem (linkItem) { 183 | return Object.assign(linkItem, { 184 | type: linkItem.items && linkItem.items.length ? 'links' : 'link' 185 | }) 186 | } 187 | 188 | /** 189 | * @param { Route } route 190 | * @param { Array | Array | [link: string]: SidebarConfig } config 191 | * @returns { base: string, config: SidebarConfig } 192 | */ 193 | export function resolveMatchingConfig (regularPath, config) { 194 | if (Array.isArray(config)) { 195 | return { 196 | base: '/', 197 | config: config 198 | } 199 | } 200 | for (const base in config) { 201 | if (ensureEndingSlash(regularPath).indexOf(encodeURI(base)) === 0) { 202 | return { 203 | base, 204 | config: config[base] 205 | } 206 | } 207 | } 208 | return {} 209 | } 210 | 211 | function ensureEndingSlash (path) { 212 | return /(\.html|\/)$/.test(path) 213 | ? path 214 | : path + '/' 215 | } 216 | 217 | function resolveItem (item, pages, base, groupDepth = 1) { 218 | if (typeof item === 'string') { 219 | return resolvePage(pages, item, base) 220 | } else if (Array.isArray(item)) { 221 | return Object.assign(resolvePage(pages, item[0], base), { 222 | title: item[1] 223 | }) 224 | } else { 225 | if (groupDepth > 3) { 226 | console.error( 227 | '[vuepress] detected a too deep nested sidebar group.' 228 | ) 229 | } 230 | const children = item.children || [] 231 | if (children.length === 0 && item.path) { 232 | return Object.assign(resolvePage(pages, item.path, base), { 233 | title: item.title 234 | }) 235 | } 236 | return { 237 | type: 'group', 238 | path: item.path, 239 | title: item.title, 240 | sidebarDepth: item.sidebarDepth, 241 | children: children.map(child => resolveItem(child, pages, base, groupDepth + 1)), 242 | collapsable: item.collapsable !== false 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | actionText: 快速阅读 → 4 | actionLink: /vue-ebook/0.md 5 | features: 6 | - title: 第一部分 7 | details: 介绍 Vue 快速入门。结合 Vue 基础、Vue 框架与设计,快速入门。 8 | - title: 第二部分 9 | details: 介绍 Vue 的正确使用方式。日常使用 Vue 中,常见方式和项目实战。 10 | - title: 实战源码开源 11 | details: 入门与实战中示例代码开源 12 | footer: MIT Licensed | Copyright ©2020-present 被删 13 | --- 14 | 15 |
16 | 17 | ## 本书说明 18 | 19 |
20 | 本书全面介绍 Vue.js 框架的出现、设计和使用,结合实战让读者更深入理解 Vue.js 框架,掌握使用方法。 21 | 22 | 该书的出现源于去年帮助团队的成员快速入门 Vue.js,在写完《9102 全员学 Vue》系列文章后,对于如何系统学习和理解 Vue.js、日常的项目管理和实践又是如何进行,依然有很多可以传授的内容。 23 | 24 | 而将这些内容整理下来,开源给到更多有需要的人便是我的本意。本书的完成特别感谢图灵出版社的张霞编辑,在她的帮助下该书有了完整的写作方向,同时十分感谢张霞编辑支持本人最终进行开源的选择。 25 | 26 | > 这本书已上架图灵电子书,欢迎阅读: [https://www.ituring.com.cn/book/2941](https://www.ituring.com.cn/book/2941) 27 | 28 |
29 |
30 | 31 |
32 | 33 | ## 本书代码实例 34 | 35 | 本书相关代码存放在[vue-sourcecode 分支](https://github.com/godbasin/vue-ebook/tree/vue-sourcecode)。 36 | 37 | ## 赞赏码 38 | 39 | **写文不易,欢迎各种花式赞赏:** 40 | 41 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/2code2.jpg) 42 | 43 |
44 |
45 | 46 | ## 版权许可 47 | 48 | 本书采用“保持署名—非商用”创意共享 4.0 许可证。 49 | 50 | 只要保持原作者署名和非商用,您可以自由地阅读、分享、修改本书。 51 | 52 | 详细的法律条文请参见[创意共享](http://creativecommons.org/licenses/by-nc/4.0/)网站。 53 | 54 |
55 | -------------------------------------------------------------------------------- /docs/vue-ebook/0.md: -------------------------------------------------------------------------------- 1 | # 前端框架的出现 2 | 3 | ## 前端的飞速发展 4 | 5 | 如果你也有经历从 jQuery 一把梭的年代,或许你也会惊讶于短短的几年时间,前端竟然经历了这么多的改变。曾经前端大多数代表着切图和重构,重页面样式而轻逻辑。 6 | 7 | 那是 node.js 刚出现还不稳定的年代,前端常常拼接 JSP 模板、拼 PHP 模板,工作大多数用来调节浏览器兼容。所以 jQuery 的出现,链式调用的编程方式使得代码如行云流水般流畅,同时提供了极容易使用的异步请求 ajax,超方便的 Sizzle 引擎元素选择器,提供的便捷几乎满足了当时前端的大部分工作,所以说 jQuery 一把梭不是毫无道理的。 8 | 9 | 相信这也是当年被迫写过 HTML/CSS/Javascript 三剑客的各种开发对前端保留的印象:只需要 jQuery 就可以了。当然毫无疑问 jQuery 是个极其优秀的库,但 node.js 的发展、npm 包管理的完善,加上活跃的开源社区,前端已经获得了千千万万开发者的支援,从页面开发到工具库开发、框架开发、脚本开发、到服务端开发,单线程的 Javascript 正在不断进行自我革新,从而将领域不断拓宽,形成了如今你所能看到的、获得赋能的前端。 10 | 11 | ### 前端工程化 12 | 13 | 我们在代码中会使用到很多的资源,图片、样式、代码,还有各式各样的依赖包,而打包的时候怎么实现按需分块、处理依赖关系、不包含多余的文件或内容,同时提供开发和生产环境,包括本地调试自动更新到浏览器这些能力,都是由工程化的工具整合起来的。在现在,这样的工具更多的表现在 gulp、webpack 这种工具上。 14 | 15 | 要实现工程化,前端开发几乎都离不开 node.js 的包管理器 npm,比如前端在搭建本地开发服务以及打包编译前端代码等都会用到。在前端开发过程中,经常用到`npm install`来安装所需的依赖。 16 | 17 | 同时,我们可以结合一些 Tree-shaking 的能力,在本地构建的时候,把使用的别人的依赖包里没用到的部分去掉,减小代码包的大小等。 18 | 19 | 依赖管理工具、自动化工具、代码规范工具、测试工具等等,层出不穷的新工具加快了前端工程化的步伐。而各种框架也各自推出了脚手架,可以让你快速地生成示例代码、搭建本地环境,也可以更新依赖的版本等,避免了每个开发者自行调整开发环境、打包逻辑等配置。 20 | 21 | 至此,开发者可以专注于业务开发,不再需要花很多事件处理一些低效而重复性的事情,开发效率得到质的提升。 22 | 23 | ## 前端框架的出现 24 | 25 | 最初是 AngularJS 开始占领了比较多的地位,后面 React 迎面赶上,在 Angular 断崖升级的帮助下,Vue 结合了各种框架的优势,以及非常容易入门的文档,成功成为了那一匹黑马。 26 | 27 | 那么这些框架它们都做了什么事情呢?为什么到现在几乎所有的前端项目都脱离不了框架开发呢?首先还是得从浏览器讲起。 28 | 29 | ### 浏览器如何渲染页面 30 | 31 | 我们知道一个页面的代码里,主要包括了 HTML、CSS、Javascript 三大块内容,那么浏览器是怎么解析和加载这些内容的呢? 32 | 33 | 一次浏览器的页面渲染过程中,浏览器会解析三种文件: 34 | 35 | - 解析 HTML/SVG/XHTML,会生成一个 DOM 结构树 36 | - 解析 CSS,会生成一个 CSS 规则树 37 | - 解析 JS,可通过 DOM API 和 CSS API 来操作 DOM 结构树和 CSS 规则树 38 | 39 | CSS 规则树与 DOM 结构树结合,最终生成一个 Render 树(即最终呈现的页面,例如其中会移除 DOM 结构树中匹配到 CSS 里面`display:none`的 DOM 节点)。一般来说浏览器绘制页面的过程是: 40 | (1) 计算 CSS 规则树。 41 | (2) 生成 Render 树。 42 | (3) 计算各个节点的大小/position/z-index。 43 | (4) 绘制。 44 | 45 | 由于篇幅的原因,这里不做过多的说明,大家感兴趣可以去细细阅读下[《How browsers work》](http://taligarsiel.com/Projects/howbrowserswork1.htm)这篇文章。 46 | 47 | ### 页面的局部刷新 48 | 49 | 一般看来,浏览器生成了最终的 Render 树,页面也已经渲染完毕,似乎浏览器已经完成了它的工作了。但现实中我们的页面更多的不只是静态的页面,还会包括点击、拖拽等事件操作,以及接口请求、数据渲染到页面等动态的交互逻辑,这时候我们会需要更新页面的信息。 50 | 51 | 我们的业务代码中情况会复杂得多,除了插入内容,还包括内容更新、删除元素节点等。不管是哪种情况,目前来说前端一般分为两种方式: 52 | (1) 字符串模版:使用拼接的方式生成 DOM 字符串,直接通过`innerHTML()`插入页面。 53 | (2) 节点模版:使用`createElement()`/`appendChild()`/`textContent`等方法,动态地插入 DOM 节点。 54 | 55 | 代码人当然要用代码来说话啦,假设页面中存在`
`这样一个元素,我们需要插入一些内容如`

测试test

`: 56 | 57 | ```js 58 | var div = document.getElementById("div"); 59 | 60 | /** 1. 字符串模版 **/ 61 | div.innerHTML = "

测试test

"; 62 | 63 | /** 2. 节点模版 **/ 64 | const p = document.createElement("p"); 65 | p.textContent = "测试"; 66 | const a = document.createElement("a"); 67 | a.textContent = "test"; 68 | p.appendChild(a); 69 | div.appendChild(p); 70 | ``` 71 | 72 | 当然这个世界不是非黑即白的,真实应用中可能也会存在两种方式都同时使用,甚至也有夹杂着使用的情况。同时,我们使用 DOM API 和 CSS API 的时候,通常会触发浏览器的两种操作:Repaint(重绘)和 Reflow(回流): 73 | 74 | - Repaint:页面部分重画,通常不涉及尺寸的改变,常见于颜色的变化。 75 | - Reflow:意味着节点需要重新计算和绘制,常见于尺寸的改变。 76 | 77 | 在 Reflow 的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成 Reflow 后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为 Repaint。 78 | 79 | 回流的花销跟 render tree 有多少节点需要重新构建有关系,所以使用`innerHTML()`可能会导致更多的开销。 80 | 81 | ### 前端框架做了什么 82 | 83 | 前端框架为什么变得如此重要,我们来看看在框架出现前,我们是如何和用户进行交互的。以一个常见的表单提交作为例子: 84 | 85 | #### (1) 编写静态页面。 86 | 87 | ```html 88 |
89 | Name: 90 |

91 | 92 | Email: 93 |

94 | 95 | 96 |
97 | ``` 98 | 99 | #### (2) 给对应的元素绑定对应的事件。 100 | 101 | 例如给 input 输入框绑定输入事件: 102 | 103 | ```js 104 | var nameInputEl = document.getElementById("name-input"); 105 | var emailInputEl = document.getElementById("email-input"); 106 | // 监听输入事件,此时 updateValue 函数未定义 107 | nameInputEl.addEventListener("input", updateNameValue); 108 | emailInputEl.addEventListener("input", updateEmailValue); 109 | ``` 110 | 111 | #### (3) 事件触发时,更新页面内容。 112 | 113 | ```js 114 | var nameValueEl = document.getElementById("name-value"); 115 | var emailValueEl = document.getElementById("email-value"); 116 | // 定义 updateValue 函数,用来更新页面内容 117 | function updateNameValue(e) { 118 | nameValueEl.innerText = e.srcElement.value; 119 | } 120 | function updateEmailValue(e) { 121 | emailValueEl.innerText = e.srcElement.value; 122 | } 123 | ``` 124 | 125 | 上面这是最简单的例子,结合前面说过的页面更新的方式,如果我们页面中有很多的内容需要更新,光拼接字符串我们可能就有一大堆代码。以下的例子,为了不占用大量的篇幅,使用了 jQuery,否则代码量会更多。 126 | 127 | 例如我们新增一个卡片,卡片内需要填写一些内容: 128 | 129 | ```js 130 | var index = 0; 131 | // 用来新增一个卡片,卡片内需要填写一些内容 132 | function addCard() { 133 | // 获取一个id为the-dom的元素 134 | var body = $("#the-dom"); 135 | // 从该元素内获取class为the-class的元素 136 | var addDom = body.find(".the-class"); 137 | // 在the-class元素前方插入一个div 138 | addDom.before('
'); 139 | // 同时保存下来该DOM节点,方便更新内容 140 | var theDom = body.find('[data-index="' + index + '"]'); 141 | theDom.innerHTML( 142 | ` 143 | 144 | 145 | ` 146 | ); 147 | // 做完上面这堆之后index自增 148 | index++; 149 | return theDom; 150 | } 151 | ``` 152 | 153 | 而当我们需要对输入框进行某些字数限制的时候: 154 | 155 | ```js 156 | // theDom使用上面代码保存下来的引用哈 157 | // 问题绑定值 158 | theDom 159 | .on("keyup", ".question", function(ev) { 160 | ev.target.value = ev.target.value.substr(0, 20); 161 | }) 162 | // 答案a绑定值 163 | .on("keyup", ".option-a", function(ev) { 164 | ev.target.value = ev.target.value.substr(0, 10); 165 | }) 166 | // 答案b绑定值 167 | .on("keyup", ".option-b", function(ev) { 168 | ev.target.value = ev.target.value.substr(0, 10); 169 | }); 170 | ``` 171 | 172 | 而当我们需要获取某些输入框内容的时候: 173 | 174 | ```js 175 | // 获取卡片的输入值 176 | // theDom使用上面代码保存下来的引用哈 177 | function getCardValue(index) { 178 | var body = $("#the-dom"); 179 | var theDom = body.find('[data-index="' + index + '"]'); 180 | var questionName = theDom.find(".question").val(); 181 | var optionA = theDom.find(".option-a").val(); 182 | var optionB = theDom.find(".option-b").val(); 183 | return { questionName, optionA, optionB }; 184 | } 185 | ``` 186 | 187 | 而当我们使用 Vue 的时候,我们可以这么写: 188 | 189 | ```html 190 | 212 | 213 | 237 | ``` 238 | 239 | 数据绑定、界面更新、事件监听等都用最简单的方式提供给开发者,开发效率和代码可维护性也是直线上升。前端框架中很重要的一个功能——模板引擎,通过 AST 它能做到的事情还包括: 240 | 241 | - 排除无效 DOM 元素,并在构建过程可进行报错 242 | - 使用自定义组件的时候,可匹配出来 243 | - 可方便地实现数据绑定、事件绑定等,具备自动更新页面的功能 244 | - 为虚拟 DOM Diff 过程打下铺垫 245 | - HTML 转义(预防 XSS 漏洞) 246 | 247 | 配合前面提到的前端工程化,前端的开发模式有很大的转变。我们来看看,Vue 框架具体做了什么,我们要怎么快速入门和上手,有哪些的方式可以高效提升开发效率、避免一些容易发生的问题,或者是脑洞大开的一些编码方式,都可以从这本书里看到,希望你们会喜欢。 248 | -------------------------------------------------------------------------------- /docs/vue-ebook/1.md: -------------------------------------------------------------------------------- 1 | # 第1章 Vue 框架介绍 2 | 3 | 根据 Vue 官方的介绍,Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。但 Vue 并不只是一个简单的视图库,通过与一系列周边工具的配合,它也可以轻易地构建大型应用。 4 | 5 | 所以 Vue 框架到底做了些什么呢?为什么几乎大多数前端都基本需要掌握呢?前言里也讲到了 jQuery 一把梭的时代,而大多数使用过 jQuery 的非前端人员,都有种“前端开发怎么这么多花样”的感觉。那要怎么理解 Vue 框架提供的能力,这些能力又是怎样提高开发人员的开发效率和代码维护性的呢? 6 | 7 | **模板引擎** 8 | 模板引擎大概是 Vue 里最主要、又最核心的一个能力。前面也讲到,在模板引擎还没有出现的时候,前端需要手动更新前端页面的内容,需要维护一大堆的 HTML 和变量拼接的动态内容,虽然 jQuery 的出现提升了 DOM 元素的操作性,但依然难以避免代码的可读性、可维护性上存在的一些问题。 9 | 10 | 以前我们更新页面的内容,大概的流程是:监听操作 -> 获取数据变量 -> 使用数据拼接成 HTML 模板 -> 将 HTML 内容塞到页面对应的地方 -> 将 HTML 片段内需要监听的点击等事件进行绑定。 11 | 12 | 这么复杂的逻辑,如今使用 Vue,就可以方便地在模板里用插值表达式`{{}}`、`v-bind`绑定变量来展示,同时配合`v-if`、`v-for`这些内置指令,就可以很方便地写出可读性和维护性都很不错的代码了。什么是插值表达式?什么是指令?这些我们会放在后面的章节里介绍。这里我们主要来介绍下 Vue 框架做了什么事情,这里先讲一下数据绑定。 13 | 14 | 我们在 Vue 里渲染一块内容,一般会有以下流程: 15 | (1) 解析语法生成 AST。 16 | (2) 根据 AST 结果,完成 data 数据初始化。 17 | (3) 根据 AST 结果和 data 数据绑定情况,生成虚拟 DOM。 18 | (4) 将虚拟 DOM 生成真正的 DOM 插入到页面中,此时页面会被渲染。 19 | 20 | 这样的过程要怎么去理解呢,我们一点点来看。 21 | 22 | ## 1.1 解析语法生成 AST 23 | 24 | 抽象语法树(Abstract Syntax Tree)也称为 AST 语法树,指的是源代码语法所对应的树状结构。也就是说,对于一种具体编程语言下的源代码,通过构建语法树的形式将源代码中的语句映射到树中的每一个节点上。 25 | 26 | 其实我们的 DOM 结构树,也是 AST 的一种,把 HTML DOM 语法解析并生成最终的页面。而模板引擎中常用的,则是将模板语法解析,分别生成 HTML DOM,使用像 HTML 拼接这样的方式(在对应的位置绑定变量、指令解析获取拼接逻辑等等),同时配合事件的管理、虚拟 DOM 的设计,可以最大化地提升页面的性能。 27 | 28 | 我们仔细看看这个过程是怎样的。 29 | 30 | ### 1.1.1 捕获特定语法 31 | 32 | 生成 AST 的过程涉及到编译器的原理,一般经过以下过程(参考维基百科): 33 | 34 | (1) 语法分析。 35 | 语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等。语法分析程序判断源程序在结构上是否正确,源程序的结构由上下文无关文法描述。例如 Vue 框架中,有`v-if`/`v-for`这样的指令,也有``这样的自定义 DOM 标签,还有`@click`/`:props`这样的简化绑定语法,模板引擎需要将它们一一解析出来,并相应地进行后续的处理。 36 | 37 | (2) 语义分析。 38 | 语义分析是编译过程的一个逻辑阶段,语义分析的任务是对结构上正确的源程序进行上下文有关性质的审查,进行类型审查。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息,一般类型检查也会在这个过程中进行。在 Vue 框架中,例如我们绑定了某个不存在的变量或者事件,又或者是使用了某个未定义的自定义组件等,都会在这个阶段进行报错提示。 39 | 40 | (3) 生成 AST。 41 | AST 的结构则根据使用者需要定义,1.1.2 节中生成的一些 AST 对象都是本人根据需要假设定义的,并不完全是 Vue 中最终的实现效果。 42 | 43 | 关于编译器的更详细的原理,大家感兴趣可以翻看其他大佬们的著作,例如很经典的编译原理。Vue 里到底是怎样处理的呢,语法分析、语义分析基本上是通过正则的方式来处理,生成 AST 的部分我们可以看一下简化后的源码,其实就是将解析出来的元素、指令、属性、父子节点关系等内容进行处理,得到一个 AST 对象: 44 | 45 | ```js 46 | /** 47 | * 将HTML编译成AST对象 48 | */ 49 | export function parse( 50 | template: string, 51 | options: CompilerOptions 52 | ): ASTElement | void { 53 | // 返回AST对象 54 | // 篇幅原因,一些前置定义省略 55 | // 此处开始解析HTML模板 56 | parseHTML(template, { 57 | expectHTML: options.expectHTML, 58 | isUnaryTag: options.isUnaryTag, 59 | shouldDecodeNewlines: options.shouldDecodeNewlines, 60 | start(tag, attrs, unary) { 61 | // 一些前置检查和设置、兼容处理此处省略 62 | // 此处定义了初始化的元素AST对象 63 | const element: ASTElement = { 64 | type: 1, 65 | tag, 66 | attrsList: attrs, 67 | attrsMap: makeAttrsMap(attrs), 68 | parent: currentParent, 69 | children: [] 70 | }; 71 | // 检查元素标签是否合法(不是保留命名) 72 | if (isForbiddenTag(element) && !isServerRendering()) { 73 | element.forbidden = true; 74 | process.env.NODE_ENV !== "production" && 75 | warn( 76 | "Templates should only be responsible for mapping the state to the " + 77 | "UI. Avoid placing tags with side-effects in your templates, such as " + 78 | `<${tag}>` + 79 | ", as they will not be parsed." 80 | ); 81 | } 82 | // 执行一些前置的元素预处理 83 | for (let i = 0; i < preTransforms.length; i++) { 84 | preTransforms[i](element, options); 85 | } 86 | // 是否原生元素 87 | if (inVPre) { 88 | // 处理元素元素的一些属性 89 | processRawAttrs(element); 90 | } else { 91 | // 处理指令,此处包括v-for/v-if/v-once/key等等 92 | processFor(element); 93 | processIf(element); 94 | processOnce(element); 95 | processKey(element); // 删除结构属性 96 | 97 | // 确定这是否是一个简单的元素 98 | element.plain = !element.key && !attrs.length; 99 | 100 | // 处理ref/slot/component等属性 101 | processRef(element); 102 | processSlot(element); 103 | processComponent(element); 104 | for (let i = 0; i < transforms.length; i++) { 105 | transforms[i](element, options); 106 | } 107 | processAttrs(element); 108 | } 109 | 110 | // 后面还有一些父子节点等处理,此处省略 111 | } 112 | // 其他省略 113 | }); 114 | return root; 115 | } 116 | ``` 117 | 118 | ### 1.1.2 DOM 元素捕获 119 | 120 | 最简单的,我们来捕获一个`
`元素,然后生成一个`
`元素。例如开发者写了以下这么一段模板,我们可以对它们进行捕获: 121 | 122 | ```html 123 |
124 | 123 125 |

456789

126 |
127 | ``` 128 | 129 | 捕获后我们或许可以得到这样的一个对象: 130 | 131 | ```js 132 | thisDiv = { 133 | dom: { 134 | type: "dom", 135 | ele: "div", 136 | nodeIndex: 0, 137 | children: [ 138 | { 139 | type: "dom", 140 | ele: "a", 141 | nodeIndex: 1, 142 | children: [{ type: "text", value: "123" }] 143 | }, 144 | { 145 | type: "dom", 146 | ele: "p", 147 | nodeIndex: 2, 148 | children: [ 149 | { type: "text", value: "456" }, 150 | { 151 | type: "dom", 152 | ele: "span", 153 | nodeIndex: 3, 154 | children: [{ type: "text", value: "789" }] 155 | } 156 | ] 157 | } 158 | ] 159 | } 160 | }; 161 | ``` 162 | 163 | 这个对象维护我们需要的一些信息,某个 HTML 元素里需要绑定哪些变量(变量更新的时候需要更新该节点内容),以怎样的方式来拼接(是否有逻辑指令,如`v-if`、`v-for`等),哪些节点绑定了怎样的事件监听事件(是否匹配一些常用的事件能力支持),所以这里 AST 能做的事情是很多了。 164 | 165 | 我们最终还是会根据 AST 对象生成真实的页面片段和逻辑,实现过程其实也是将很多的特殊标识(例如元素 ID、属性标记等)打到该元素上,同时配合一些 Javascript 的元素选择方式、事件监听方式等,来将这个元素动态化(支持内容更新、节点更新)而实现最终的页面效果。Vue 会根据 AST 对象生成一段可执行的代码,我们看看这部分的实现: 166 | 167 | ```js 168 | // 生成一个元素 169 | function genElement(el: ASTElement): string { 170 | // 根据该元素是否有相关的指令、属性语法对象,来进行对应的代码生成 171 | if (el.staticRoot && !el.staticProcessed) { 172 | return genStatic(el); 173 | } else if (el.once && !el.onceProcessed) { 174 | return genOnce(el); 175 | } else if (el.for && !el.forProcessed) { 176 | return genFor(el); 177 | } else if (el.if && !el.ifProcessed) { 178 | return genIf(el); 179 | } else if (el.tag === "template" && !el.slotTarget) { 180 | return genChildren(el) || "void 0"; 181 | } else if (el.tag === "slot") { 182 | return genSlot(el); 183 | } else { 184 | // component或者element的代码生成 185 | let code; 186 | if (el.component) { 187 | code = genComponent(el.component, el); 188 | } else { 189 | const data = el.plain ? undefined : genData(el); 190 | 191 | const children = el.inlineTemplate ? null : genChildren(el, true); 192 | code = `_c('${el.tag}'${ 193 | data ? `,${data}` : "" // data 194 | }${ 195 | children ? `,${children}` : "" // children 196 | })`; 197 | } 198 | // 模块转换 199 | for (let i = 0; i < transforms.length; i++) { 200 | code = transforms[i](el, code); 201 | } 202 | // 返回最后拼装好的可执行的代码 203 | return code; 204 | } 205 | } 206 | ``` 207 | 208 | ### 1.1.3 模板引擎赋能 209 | 210 | 原本就是一个`
`,经过 AST 生成一个对象,最终还是生成一个`
`,这是多余的步骤吗?不是的,在这个过程中我们可以实现一些功能: 211 | (1) 排除无效 DOM 元素,并在构建过程可进行报错。 212 | (2) 使用自定义组件的时候,可匹配出来。 213 | (3) 可方便地实现数据绑定、事件绑定等功能。 214 | (4) 为虚拟 DOM Diff 过程打下铺垫。 215 | (5) HTML 转义(预防 XSS 漏洞)。 216 | 217 | 通过通用的模板引擎,我们能处理掉很多低效又重复的工作,例如浏览器兼容、全局事件的统一管理和维护、模板更新的虚拟 DOM 机制、树状组织管理组件,而留给开发者的事情就可以少之又少。现在我们知道了模板引擎都做了什么事情,就可以区分开 Vue 框架提供的能力和我们需要自行处理的逻辑,可以更专注于业务开发。 218 | 219 | 我们来看看虚拟 DOM 的机制。 220 | 221 | ## 1.2 虚拟 DOM 222 | 223 | 虚拟 DOM 如今都被作为前端面试基础题库之一了,多多少少面试者都要去了解下,当初 React 就是因为虚拟 DOM 的提出,暂时赢下了浏览器性能之争(尤其在 AngularJS 的脏检查机制对比下)。当然,这都是几年前的事情了,如今几大框架的性能问题,早也不是什么大问题了。 224 | 225 | 虚拟 DOM 大概可分成三个过程: 226 | (1) 用 JS 对象模拟 DOM 树,得到一棵虚拟 DOM 树。 227 | (2) 当页面数据变更时,生成新的虚拟 DOM 树,比较新旧两棵虚拟 DOM 树的差异。 228 | (3) 把差异应用到真正的 DOM 树上。 229 | 230 | ### 1.2.1 用 JS 对象模拟 DOM 树 231 | 232 | 为什么要用到虚拟 DOM 呢?不知道大家仔细研究过 DOM 节点对象没,一个真正的 DOM 元素非常庞大,拥有很多的属性值。因为一个 DOM 节点它包括了太多太多的属性、元素和事件对象,实际上我们并不是全部都会用到,通常包括节点内容、元素位置、样式、节点的添加删除等方法。所以,我们通过用 JS 对象表示 DOM 元素的方式,可以大大降低了比较差异的计算量。 233 | 234 | 看一下 Vue 源码,我们会发现 VNode 只有这些属性: 235 | 236 | ```js 237 | tag: string | void; 238 | data: VNodeData | void; 239 | children: ?Array; 240 | text: string | void; 241 | elm: Node | void; 242 | ns: string | void; 243 | context: Component | void; // rendered in this component's scope 244 | key: string | number | void; 245 | componentOptions: VNodeComponentOptions | void; 246 | componentInstance: Component | void; // component instance 247 | parent: VNode | void; // component placeholder node 248 | // strictly internal 249 | raw: boolean; // contains raw HTML? (server only) 250 | isStatic: boolean; // hoisted static node 251 | isRootInsert: boolean; // necessary for enter transition check 252 | isComment: boolean; // empty comment placeholder? 253 | isCloned: boolean; // is a cloned node? 254 | isOnce: boolean; // is a v-once node? 255 | asyncFactory: Function | void; // async component factory function 256 | asyncMeta: Object | void; 257 | isAsyncPlaceholder: boolean; 258 | ssrContext: Object | void; 259 | fnContext: Component | void; // real context vm for functional nodes 260 | fnOptions: ?ComponentOptions; // for SSR caching 261 | devtoolsMeta: ?Object; // used to store functional render context fordevtools 262 | fnScopeId: ?string; // functional scope id support 263 | ``` 264 | 265 | ### 1.2.2 比较新旧两棵虚拟 DOM 树的差异 266 | 267 | 虚拟 DOM 中,差异对比也是很关键的一步,这里简单说一下。当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。通常来说这样的差异需要记录: 268 | 269 | - 需要替换掉原来的节点 270 | - 移动、删除、新增子节点 271 | - 修改了节点的属性 272 | - 对于文本节点的文本内容改变 273 | 274 | ![](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/wxapp-4.png) 275 | 图 1-1 DOM 树对比 276 | 277 | 如图 1-1,这里我们对比两棵 DOM 树,得到的差异有:p 元素插入了一个 span 元素子节点,然后原先的文本节点挪到了 span 元素子节点下面。最后通过 JS 操作就可以实现完,具体实现的步骤则是第三步: 278 | 279 | ### 1.2.3 把差异应用到真正的 DOM 树上 280 | 281 | 经过差异对比之后,我们能获得一组差异记录,接下里我们需要使用它。差异记录要应用到真正的 DOM 树上,需要进行一些操作,例如节点的替换、移动、删除,文本内容的改变等。 282 | 283 | 我们来看一下,在 Vue 中是怎么进行 DOM Diff 的,虽然代码里很多函数没贴出来,但其实看函数名也可以大概理解都是什么作用,例如`updateChildren`、`addVnodes`、`removeVnodes`、`setTextContent`等,大致是个对比差异然后更新的操作: 284 | 285 | ```js 286 | // 简单看这段代码感受下 287 | // 对比差异然后更新 288 | const oldCh = oldVnode.children; 289 | const ch = vnode.children; 290 | if (isDef(data) && isPatchable(vnode)) { 291 | for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); 292 | if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode); 293 | } 294 | if (isUndef(vnode.text)) { 295 | if (isDef(oldCh) && isDef(ch)) { 296 | if (oldCh !== ch) 297 | updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); 298 | } else if (isDef(ch)) { 299 | if (process.env.NODE_ENV !== "production") { 300 | checkDuplicateKeys(ch); 301 | } 302 | if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, ""); 303 | addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); 304 | } else if (isDef(oldCh)) { 305 | removeVnodes(elm, oldCh, 0, oldCh.length - 1); 306 | } else if (isDef(oldVnode.text)) { 307 | nodeOps.setTextContent(elm, ""); 308 | } 309 | } else if (oldVnode.text !== vnode.text) { 310 | nodeOps.setTextContent(elm, vnode.text); 311 | } 312 | if (isDef(data)) { 313 | if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode); 314 | } 315 | ``` 316 | 317 | 前言中我们提到,页面的更新有两种方式:字符串模板和节点模板。而字符串模板`innerHTML`的方式会消耗比较多的计算,节点模板的方式又需要管理特别多的节点信息和引用保存,使用虚拟 DOM 则可以有效地降低浏览器计算和性能。 318 | 319 | ## 1.3 数据绑定 320 | 321 | 在 Vue 中,最基础的模板语法是数据绑定,例如: 322 | 323 | ```html 324 |
{{ message }}
325 | ``` 326 | 327 | 这里使用插值表达式`{{}}`绑定了一个`message`的变量,开发者在 Vue 实例`data`中绑定该变量: 328 | 329 | ```js 330 | new Vue({ 331 | data: { 332 | message: "测试文本" 333 | } 334 | }); 335 | ``` 336 | 337 | 最终页面展示内容为`
测试文本
`。这是怎么做到的呢? 338 | 339 | ### 1.3.1 数据绑定的实现 340 | 341 | 这种使用双大括号来绑定变量的方式,我们称之为数据绑定。它是怎么实现的呢,前面也有大致提到,数据绑定的过程其实不复杂: 342 | (1) 解析语法生成 AST。 343 | (2) 根据 AST 结果生成 DOM。 344 | (3) 将数据绑定更新至模板。 345 | 346 | 上述这个过程,是 Vue 中模板引擎在做的事情。我们来看看上面在 Vue 里的代码片段`
{{ message }}
`,我们可以通过 DOM 元素捕获,解析后获得这样一个 AST 对象: 347 | 348 | ```js 349 | thisDiv = { 350 | dom: { 351 | type: "dom", 352 | ele: "div", 353 | nodeIndex: 0, 354 | children: [{ type: "text", value: "" }] 355 | }, 356 | binding: [{ type: "dom", nodeIndex: 0, valueName: "message" }] 357 | }; 358 | ``` 359 | 360 | 这样,我们在生成一个 DOM 的时候,同时添加对`message`的监听,数据更新时我们会找到对应的`nodeIndex`,更新值: 361 | 362 | ```js 363 | // 假设这是一个生成 DOM 的过程,包括 innerHTML 和事件监听 364 | function generateDOM(astObject) { 365 | const { dom, binding = [] } = astObject; 366 | // 生成DOM,这里假装当前节点是baseDom 367 | baseDom.innerHTML = getDOMString(dom); 368 | // 对于数据绑定的,来进行监听更新吧 369 | baseDom.addEventListener("data:change", (name, value) => { 370 | // 寻找匹配的数据绑定 371 | const obj = binding.find(x => x.valueName == name); 372 | // 若找到值绑定的对应节点,则更新其值。 373 | if (obj) { 374 | baseDom.find(`[data-node-index="${obj.nodeIndex}"]`).innerHTML = value; 375 | } 376 | }); 377 | } 378 | 379 | // 获取DOM字符串,这里简单拼成字符串 380 | function getDOMString(domObj) { 381 | // 无效对象返回'' 382 | if (!domObj) return ""; 383 | const { type, children = [], nodeIndex, ele, value } = domObj; 384 | if (type == "dom") { 385 | // 若有子对象,递归返回生成的字符串拼接 386 | const childString = ""; 387 | children.forEach(x => { 388 | childString += getDOMString(x); 389 | }); 390 | // dom对象,拼接生成对象字符串 391 | return `<${ele} data-node-index="${nodeIndex}">${childString}`; 392 | } else if (type == "text") { 393 | // 若为textNode,返回text的值 394 | return value; 395 | } 396 | } 397 | ``` 398 | 399 | 这样,我们就能在`message`变量更新的时候,通过该变量关联的引用,来自动更新对应展示的内容。而要知道`message`变量什么时候进行了改变,我们需要对数据进行监听。 400 | 401 | ### 1.3.2 数据更新监听 402 | 403 | 我们能看到,上面的简单代码描述过程中,使用的数据监听方法是用了`addEventListener("data:change", Function)`的方式。其实每个框架都会有自己的方式,例如 Angular 使用的则是大家都熟知的脏检查。 404 | 405 | **(1) Angular: watch 机制。** 406 | 脏检查是什么呢?在 AngularJS 中,并不直接监听数据的变动,而是监听常见的事件如用户交互(点击、输入等)、定时器、生命周期等。在每次事件触发完毕后,计算数据的新值和旧值是否有差异,若有差异则更新页面,并触发下一次的脏检查,直到没有差异或是次数达到设定阈值。很显然,这样每一次脏检查可能会形成环状,导致死循环。虽然 AngularJS 有阈值控制,但也无法避免这种机制所导致的低效甚至性能问题。 407 | 408 | 由于设计上存在的这些问题一直被大家诟病,在 Angular2 中应用的组织类似 DOM,也是树结构的,脏检查会从根组件开始,自上而下对树上的所有子组件进行检查。相比 AngularJS 中的带有环的结构,这样的单向数据流效率更高,而且容易预测,性能上也有不少的提升。 409 | 410 | **(2) React: 手动 set 写入。** 411 | 在 React 里,是需要手动调用`set()`的方式写入数据来更新模版。 412 | 413 | **(3) Vue: Getter/Setter。** 414 | 而在 Vue 中,主要是依赖了`Getter/Setter`: 415 | 416 | ```js 417 | Object.defineProperty(obj, key, { 418 | enumerable: true, 419 | configurable: true, 420 | // getter 421 | get: function reactiveGetter() { 422 | const value = getter ? getter.call(obj) : val; 423 | if (Dep.target) { 424 | dep.depend(); 425 | if (childOb) { 426 | childOb.dep.depend(); 427 | if (Array.isArray(value)) { 428 | dependArray(value); 429 | } 430 | } 431 | } 432 | return value; 433 | }, 434 | // setter,最终更新后会通知噢 435 | set: function reactiveSetter(newVal) { 436 | const value = getter ? getter.call(obj) : val; 437 | if (newVal === value || (newVal !== newVal && value !== value)) { 438 | return; 439 | } 440 | if (process.env.NODE_ENV !== "production" && customSetter) { 441 | customSetter(); 442 | } 443 | if (getter && !setter) return; 444 | if (setter) { 445 | setter.call(obj, newVal); 446 | } else { 447 | val = newVal; 448 | } 449 | childOb = !shallow && observe(newVal); 450 | dep.notify(); 451 | } 452 | }); 453 | ``` 454 | 455 | 在数据更新的时候就执行了模板更新、watch、computed 等一些工作,而 Vue3.0 将使用`Proxy`的方式来进行,可参考[《第16章 关于 Vue 3.0》](./16.md)。 456 | 457 | ## 1.4 XSS 漏洞填补 458 | 459 | 模板引擎还可以协助预防下 XSS 相关漏洞。我们知道,XSS 的整个攻击过程大概为: 460 | (1) 通常页面中包含的用户输入内容都在固定的容器或者属性内,以文本的形式展示。 461 | (2) 攻击者利用这些页面的用户输入片段,拼接特殊格式的字符串,突破原有位置的限制,形成了代码片段。 462 | (3) 攻击者通过在目标网站上注入脚本,使之在用户的浏览器上运行,从而引发潜在风险。 463 | 464 | 避免 XSS 的方法之一主要是将用户所提供的内容进行过滤,而大多数模板引擎会自带 HTML 转义功能。在 Vue 中,默认的数据绑定方式(双大括号、`v-bind`等)会进行 HTML 转义,将数据解释为普通文本,而非 HTML 代码。来感受下这堆正则和转义,篇幅关系,只贴一部分来观摩下: 465 | 466 | ```js 467 | // 元素标签和属性相关的正则表达式 468 | const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; 469 | const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; 470 | const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z${unicodeRegExp.source}]*`; 471 | const qnameCapture = `((?:${ncname}\\:)?${ncname})`; 472 | const startTagOpen = new RegExp(`^<${qnameCapture}`); 473 | const startTagClose = /^\s*(\/?)>/; 474 | const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); 475 | const doctype = /^]+>/i; 476 | // 避免注释中被处理掉 477 | const comment = /^", 488 | """: '"', 489 | "&": "&", 490 | " ": "\n", 491 | " ": "\t", 492 | "'": "'" 493 | }; 494 | ``` 495 | 496 | 当然,如果你一定要输出 HTML 代码,也可以使用`v-html`指令输出。官方文档也有特殊说明:你的站点上动态渲染的任意 HTML 可能会非常危险,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要对用户提供的内容使用插值。 497 | 498 | Vue 中大多数能力都依赖于模板引擎,包括组件化管理、事件管理、Vue 实例、生命周期等等,更多的能力阅读,大家感兴趣的可以去翻一下 Vue 源码,相信你理解了 AST、虚拟 DOM、数据绑定相关的机制之后,再去翻阅源码看具体的实现也不再是难事了。 499 | -------------------------------------------------------------------------------- /docs/vue-ebook/10.md: -------------------------------------------------------------------------------- 1 | # 第10章 如何正确地进行抽象 2 | 3 | 关于抽象,它其实是一个通用的能力。而掌握了抽象的能力后,当你用到应用、页面中,不管是组件化、配置化还是数据流等处理,都可以水到渠成。 4 | 5 | 对于写业务代码,很多前端都觉得枯燥无趣,且认为容易达到技术瓶颈。其实并不是这样的,几乎所有被我们称之为“技术需求”、“技术工具”的开发,它都来自于业务的需要,Vue 也是。而在前端领域,业务开发就真的只是调节样式、拼接模板、绑定事件、接口请求、更新页面这些内容吗?其实也不是的,在学习完本章之后,你会发现前端的世界也可以这么精彩,而 Vue 也可以这么好玩。 6 | 7 | 我们下面将按照将页面划分成模块、模块抽象成数据、对应用进行配置化,以及组件的抽象、组件配置化的顺序,来探索这样一种新玩法吧。 8 | 9 | ## 10.1 页面划分成模块 10 | 11 | 产品在设计一个页面的时候,会根据内容和功能的不同,设计出不同的模块,然后再拼凑成页面。对于前端同学来说,拿到一个设计好的交互稿或者设计图之后,需要进行逆向拆解,我们要把一个页面按照功能和内容划分出一个个的模块。而我们拆出来的模块并不一定完全跟产品设计的一致,会根据不同的粒度、视觉和易抽象程度来进行划分。 12 | 13 | ### 10.1.1 什么是模块 14 | 15 | 我们来看看常见的应用页面,这里我截取了自己的博客来进行说明: 16 | 17 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-10-1.jpg) 18 | 图 10-1 博客页面 19 | 20 | 我们可以直观地根据视觉感受来划分下: 21 | 22 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-10-2.jpg) 23 | 图 10-2 博客页面模块划分 24 | 25 | 大致可以分为三大块: 26 | 27 | - 头部:快速导航栏 28 | - 左侧:内容板块 29 | - 右侧:推广导航板块 30 | 31 | 其实论坛类、博客类的页面大多如此,我们再来看看用 Vuepress 搭建的前端游乐场(跟 Vue 官网很像): 32 | 33 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-10-3.jpg) 34 | 图 10-3 前端游乐场页面 35 | 36 | 除此之外,还有视频类、电商类等各种角色的网站,大家有空也可以去看看,思考下里面是怎么划分的。或许你会觉得,想这些有什么用呢?这对我们平时的工作有什么帮助吗?其实`观察 -> 思考 -> 总结`也是有意思的事情,可以多一种角度来思考自己的工作内容,也能提高写代码的趣味性。如果你要认真地把这个过程放置到你的工作中,也可以找到很多提升工作效率的方法,也会让你的路越走越顺畅。 37 | 38 | 模块的划分,其实最终在代码中呈现出来的,常常是组件的划分。 39 | 40 | ### 10.1.2 组件与模块 41 | 42 | [《第4章 Vue 组件的使用》](./4.md)中,我们详细地介绍了组件。虽然组件和模块是不一样的两个概念,但是模块有些时候也可以作为一个组件来维护,而模块也可以是属于某个组件、或是包含哪些组件的关系。模块更多是在是视觉上呈现的划分,而组件则更偏向功能上的划分。一个模块是否可以成为一个组件,需要看这个模块是否拥有属于自己的状态、数据、事件等对于组件的封装也都已经在 4.4 章节中有详细的描述。 43 | 44 | ## 10.2 模块抽象成数据 45 | 46 | 想象一下,在把数据与逻辑分离到极致的时候,我们看一个应用/页面,会看到一具静态的逻辑躯壳,以及动态的数据组成。数据如灵魂般地注入到应用/页面中,可使其获得生命。关于如何进行数据的抽离,通常来说可以把变化的部分分离和抽象,然后通过注入的方式,来实现具体的功能和展示。 47 | 48 | 是否有点抽象?这样的一个分离过程,也可以理解为我们写好的一个页面,需要从后台获取到数据,然后根据数据渲染出对应的内容。在这里,页面就是静态的,而获取的数据就是动态的。从另外一个角度来说,除了后台请求的数据,我们在 Vue 中通过`data`绑定的数据都可以抽离。关于这些可抽离的数据,我们来简单识别和划分一下。 49 | 50 | ### 10.2.1 状态数据 51 | 52 | 在一个应用的设计里,我们可能会拥有多个组件,每个组件又各自维护着自己的某些状态,同时部分状态相互影响着,叠加起来呈现出应用最终的整体状态。这些状态,都可以通过数据的方式来表示,我们简单称之为**状态数据**。怎么定义状态数据?最浅显或是最直观的办法就是,这些数据可以直接影响页面的呈现,如对话框的出现、隐藏,标签的激活、失活,长流程中的进行中步骤等,都可以作为状态数据。在 Vue 里面,状态数据会经常与 `v-show`、`v-if`等逻辑结合使用。 53 | 54 | 我们的应用,大多数都是呈现树状结构,一层层地往下分解,直到无法分割的某个简单功能。同时,我们的组件也会呈现出来这样树状的方式,状态是跟随着组件维护,某个功能状态属于组件自己,最外层的状态则属于整个应用,当然这个应用可以看做是一个组件。 55 | 56 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-10-9.jpg) 57 | 图 10-4 博客模块划分 58 | 59 | 如图 10-4,图中的每个模块都可以附着着一个“是否可见”的状态。我们的应用状态整体上也是会呈现树状的方式,与我们的组件相对应,就像 DOM 节点树、CSS 规则树和渲染树的关系。 60 | 61 | ### 10.2.2 动态数据 62 | 63 | 我们还有很多的数据,如内容、个人信息等,都是需要我们从数据库拉取回来的。这种需要动态获取然后用于展示或是影响展示的一些数据,我们可以称作动态数据。动态数据不同于状态数据,并不会跟随着应用的生命周期而改变,也不会随着应用的关闭而消失。它们独立存在于外界,通过注入的方式进入应用,并影响具体的展示和功能逻辑。 64 | 65 | 和状态数据不一样,动态数据并不一定呈现为树状的形式。它可以是并行的,可以是联动关系,随着注入的地方不一样,最终在应用中形成的结构也会不一致。我们可以简单理解为每个动态数据都是平等的。 66 | 67 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-10-10.jpg) 68 | 图 10-5 文章列表 69 | 70 | 如图 10-5,这里每篇文章内容,都是单独的一份从后台请求的数据注入。其实博客通常是静态模板,不存在从后台请求的情况,这里打个比喻,大家可以想象下社区里的文章、知乎帖子、微博等等。 71 | 72 | ### 10.2.3 将数据与应用抽离 73 | 74 | 要怎么理解将数据与应用抽离呢?形象点形容,就像是我们一个公司,所有的桌子椅子装修和电脑都是静态的,它们相当于一个个的组件,同时每个办公室也可以是一个大点的组件或是模块。那么在我们这个公司里: 75 | 76 | - 状态数据:椅子的位置、消耗的电量、办公室的照明和空调状态等 77 | - 动态数据:员工等各种人员流动 78 | 79 | 当然,公司里没有人员流动的时候,似乎就是个空壳。每天上班的时候,一个个的程序员来到公司里,给公司注入灵魂,公司得以运作。要说将数据和应用抽离,作用到这个例子中大概是这个样子的: 80 | 81 | ```cmd 82 | # 将公司和人分开(下班后) 83 | -------------------------------------------------------- 84 | 公司 85 | --------------------------- --------------------------- 86 | | | 人 人 87 | | | 人 人 88 | | 办公楼 | 人 89 | | | 人 人 人 人 90 | | | 人 人 人 91 | --------------------------- --------------------------- 92 | 93 | 94 | # 在公司正常运作的时候 95 | -------------------------------------------------------- 96 | 公司 97 | -------------------------------------------------------- 98 | | 人 人 人 人 人 人 人 | 99 | | 人 人 人 人 人 | 100 | | 人 人 办公楼 人 人 人 | 101 | | 人 人 人 人 人 人 人 | 102 | | 人 人 人 人 人 人 人 | 103 | -------------------------------------------------------- 104 | ``` 105 | 106 | 当然,人不只是站在办公楼里面这么简单,更多的,人会与各种物件进行交互和反馈,人与人之间也会相互交流和影响。但是这样简单的管理,很容易造成公司的混乱,所以我们会把人员有规律有组织地分别隔离到每个办公室、隔间里面: 107 | 108 | ```cmd 109 | # 按照组织进行分隔 110 | -------------------------------------------------------- 111 | 公司 112 | -------------------------------------------------------- 113 | | 人 | 人 人 | | 人 人 | 人 人 | 114 | | 人 | 人 | | 人 人 | 人 人 | 115 | |-------- 人 人 | 办公楼 | 人 人 --------- | 116 | | 人 | 人 | | 人 人 | 人 人 | 117 | | 人 | 人 人 | | 人 人 | 人 人 | 118 | -------------------------------------------------------- 119 | ``` 120 | 121 | 这就是我们要做的,不只是如何划分数据、将数据与应用抽离,我们还需要将其有规律地管理。所以,这大概是接下来的要讲的内容。我们知道哪些数据需要抽离、如何将数据抽离出来,同时,我们还需要知道,这些数据在抽离出来之后,该怎么去进行管理。 122 | 123 | ### 10.2.4 适度的管理 124 | 125 | 与组件的封装不适宜过度一样,数据的抽象、隔离、管理,也是需要适度的。当我们的应用很小,只有简单的功能的时候,我们甚至不需要对这些状态、数据什么的进行特殊的管理,甚至几个简单的变量就可以搞定了。随着应用组件数量变多,我们开始有了组件的作用域,当组件需要通信,我们可以通过简单的事件机制、或是共享对象的方式来进行交互。 126 | 127 | 当我们的项目越做越大,要在上百的状态、上万的数据里要按照想要的方式去展示我们的应用,这时候一个状态管理工具则可以轻松解决乱糟糟的数据流问题。关于在 Vue 中怎样进行数据和状态管理的更多内容,会在 11 章讲述。 128 | 129 | ## 10.3 深入理解配置化 130 | 131 | 配置化的思想,如今也不仅仅存在于前端或者是某个领域。所有的系统和架构设计,都可以用领域抽象、数据抽离、配置化等方式,搭建灵活配置、模块解耦的系统,前端也不例外。 132 | 133 | ### 10.3.1 可配置的数据 134 | 135 | 数据的配置,或许大家会比较熟悉,我们很多的管理端都是用来进行数据配置的。而数据配置的最终效果,则包括影响展示端的页面内容、应用的状态控制等。例如文案、活动、功能展示,都可以通过数据配置进行控制。 136 | 137 | #### 应用中的可配置数据 138 | 139 | 最常见的数据配置,大概是前面说过的一些内容配置,文案、说明等,为此还产生了运营这样的职位。常见的运作方式,是搭起一整套的运营管理平台,除了一些简单的文字或是数据以外,广告内容、推荐位等,都可以通过平台进行配置。 140 | 141 | #### 代码中的可配置数据 142 | 143 | 有些时候,我们也会在代码里面抽象出一些可配置的数据。例如,这个需求产品要求查询一周的数据,我们在开发的时候并不会将 7 天写死在涉及计算的每行代码中,而是将天数配置为 7 天,设置成全局变量: 144 | 145 | ```javascript 146 | const QUERY_DAY_NUM = 7; 147 | ``` 148 | 149 | 这样,当需要在紧急情况支持其他天数(五一、国庆、过年等假期)的时候,我们就可以只需要改动这里就可以了。更方便的情况是,这个数据的配置可以放在管理端,通过管理端下发到后台,前端展示的时候只需要从后台获取具体的天数就可以了。 150 | 151 | #### 文件里的可配置数据 152 | 153 | 虽然我们可配置的数据单独抽出来维护,但常常是将这个配置也直接写到代码里。那么如果我们需要调整这些配置,调整后还需要重新打包部署,这种情况开销大、效率低。所以在一些时候,我们会把这样的可配置数据,单独写到某个文件里维护,这个文件不合我们的代码打包到一起。当需要调整的时候,只需要单独下发一个配置文件就好了。 154 | 155 | ### 10.3.2 可配置的接口 156 | 157 | 关于接口的配置化,目前来说见过的不是特别多。毕竟现实场景中,我们的很多数据和接口并不是简单的增删查改这样的功能,很多时候还需要在接口返回前后,做一系列的逻辑处理。简单地说,很多的业务接口场景复用性不高,前后端除去协议、基础规范的定义之后,很少再能进行更深层次的抽象,导致接口配置化的改造成本较大。 158 | 159 | 配置化的实现有两点很重要的东西:**规范**和**解决方案**。如果说目前较好的从前端到后台的规范,可能是 GraphQL 和 Restful 了,大家不熟悉的也可以去看看。当然,或许有些团队已经实现了,也希望能看到一些相关的解决方案。 160 | 161 | ### 10.3.3 可配置的页面 162 | 163 | 页面的配置化,可能也已经不少见了。像我刚出道的时候,也写过一个拖拽的 Demo(如图 10-6),当时自己实现完,信心倍增。大概每个前端的成长过程中,都会伴随着一个管理端配置化的需求吧。 164 | 165 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/dd_custom_pages.png) 166 | 图 10-6 拖拽生成 H5 的 Demo 167 | 168 | 有些时候,一些页面比较简单,里面的板块、功能比较相似,可能文案不一致、模块位置调整了、颜色改变等等。虽然说复制粘贴再改一改,很多时候也能满足要求,但是我们通过抽象和配置化,就可以把重复性的工作交给机器,省下来的精力可以做更多富有创造性的工作。这种页面的配置,基本上有两种实现方式: 169 | (1) 配置后生成静态页面的代码,直接加载生成的页面代码。 170 | (2) 写通用的配置化逻辑,在加载页面的时候拉取配置数据,动态生成页面。 171 | 172 | 基于 SEO 和实现复杂度各种情况,第一种方式大概是目前比较常用的,第二种的实现难度会稍微大一些。第一种方式,很多适用于一些移动端的模版页面开发,例如简单的活动页面、商城页面等等。第二种的话,更多的是一些管理平台的实现,毕竟大多数都是增删查改,形式无非列表、表单和菜单等。配置化的核心大概是场景分析和功能拆解,所以抛开使用场景来做一个所谓“通用”的配置化是不现实的。但是如果把问题范围局限在解决特定的场景,就可以做出合适的配置化功能。 173 | 174 | ## 10.4 组件配置化 175 | 176 | 这里我们来讲一下简单的配置化组件的实现,关于组件的封装前面我们也讲过了。下面的组件配置化实现说明,我们拿这样一个卡片组件来作为例子: 177 | 178 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-10-16.jpg) 179 | 图 10-7 卡片组件样式 180 | 181 | ### 10.4.1 可配置的数据 182 | 183 | 首先是数据的配置,这大概是最基础的。当我们在封装组件的时候,很多数据都是通过作用域内的变量来动态绑定的,例如 Vue 里面则是通过`data`、`props`、`computed`等实例属性来维护 scope 内的数据绑定。作为一个卡片,内容是从外面注入的,所以我们这里使用`props`来获取: 184 | 185 | ```html 186 | 202 | 224 | ``` 225 | 226 | 上面只简单地实现部分的卡片内容,我们在使用的时候,只需要将数据传入到这个组件中就可以了: 227 | 228 | ```html 229 | 230 | ``` 231 | 232 | 在这里,`cardInfo`就是我们用来配置卡片内容的数据,我们可以从后台拉取了所有卡片的列表信息,然后配合`v-for`来绑定和生成每一个卡片内容。 233 | 234 | ### 10.4.2 可配置的样式 235 | 236 | 样式的配置,通常是通过`class`来实现的。其实这更多地是对 CSS 进行配置化设计,与我们的 HTML 和 Javascript 关系则比较少。样式的配置,需要我们考虑 CSS 的设计,通常来说我们有两种方式: 237 | (1) 根据子元素匹配,来描述 CSS。 238 | (2) 根据子 class 匹配,来描述 CSS。 239 | 240 | #### 根据子元素配置 CSS 241 | 242 | 这是以前比较常用的一种方式,简单地说,就是通过 CSS 匹配规则中的父子元素匹配,来完成我们的样式设计。例如,我们有个模块: 243 | 244 | ```html 245 |
246 |
I am header.
247 |
248 | blablablabla... 249 |
250 |
251 | 252 |
253 |
254 | ``` 255 | 256 | 样式则会这样设计: 257 | 258 | ```CSS 259 | .my-dialog { 260 | background: white; 261 | } 262 | .my-dialog > header {} 263 | .my-dialog > section {} 264 | .my-dialog > footer {} 265 | ``` 266 | 267 | 或者说用 LESS 或是 SASS: 268 | 269 | ```LESS 270 | .my-dialog { 271 | background: white; 272 | > header {} 273 | > section {} 274 | > footer {} 275 | } 276 | ``` 277 | 278 | 通过这种方式设计,或许我们在写代码的时候会稍微方便些,但是在维护上面很容易踩坑。只需要调整一次页面的 DOM 结构,就可以让你改 CSS 改到崩溃。 279 | 280 | #### 根据子 class 配置 CSS 281 | 282 | 其实相对于匹配简单的父子和后代元素关系,使用 class 来辅助匹配,可以解决 DOM 调整的时候带来的问题。这里我们使用 BEM 作为例子来解释下大概的想法吧。BEM 的意思就是块(block)、元素(element)、修饰符(modifier),是一种前端命名方法论。大家感兴趣可以去搜一下。简单说,我们写 CSS 的时候就是这样的: 283 | 284 | ```CSS 285 | .block{} 286 | .block__element{} 287 | .block--modifier{} 288 | ``` 289 | 290 | 表 10-1 BEM 命名规范 291 | 292 | | 命名 | 说明 | 举例 | 293 | | ---------- | -------------------------------- | --------------------- | 294 | | B-block | 块
可以与组件和模块对应的命名 | 如 card、dialog 等 | 295 | | E-element | 元素 | 如 header、footer 等 | 296 | | M-modifier | 修饰符
可视作状态等描述 | 如 actived、closed 等 | 297 | 298 | 这样的话,我们上述的代码则会变成: 299 | 300 | ```html 301 |
302 |
I am header.
303 |
304 | blablablabla... 305 |
306 |
307 | 308 |
309 |
310 | ``` 311 | 312 | 搭配 LESS 的话,其实样式还是挺容易写的: 313 | 314 | ```LESS 315 | .my-dialog { 316 | background: white; 317 | &__header {} 318 | &__section {} 319 | &__footer {} 320 | &__btn { 321 | &--inactived 322 | } 323 | } 324 | ``` 325 | 326 | 其实大家看了下,就发现这样的弊端了。我们在写 HTML 的时候,需要耗费很多的时间来写这些 class 名字。更麻烦的的是,当我们需要切换某个元素状态的时候,判断条件会变得很长,像: 327 | 328 | ```HTML 329 | 330 | ``` 331 | 332 | 这样写太长了,维护性上、可读性上都不大友好。当然我们还可以这样使用: 333 | 334 | ```HTML 335 | 336 | 337 | 338 | 339 | ``` 340 | 341 | 这样会稍微好一些。BEM 的优势和弊端也都是很明显的,大家也可以根据具体的团队规模、项目规模、使用场景等,来决定要怎么设计。当然,如今很多框架都支持样式的作用域,通常是通过在 class 里添加随机 MD5 等,来保持局部作用域的 class 样式,或者也可以使用 Shawdow DOM 来进行隔离。 342 | 343 | ### 10.4.3 可配置的状态和展示 344 | 345 | 可配置的状态和展示,更多时候是指某些模块的状态、展示的效果又是如何等。例如,我们需要一个对话框,其头部、正文文字、底部按钮等功能都可支持配置: 346 | 347 | ```HTML 348 |
349 |
{{cardInfo.title}}
350 |
{{cardInfo.content}}
351 |
352 | 353 |
354 |
355 | ``` 356 | 357 | 我们可以通过`cardInfo.title`来控制是否展示头部,可以通过`cardInfo.buttons`来控制底部按钮的数量和文字。这只是最简单的实例,我们可以通过配置,来控制出完全不一样的展示效果。搭配样式的配置,更是能让组件出神入化。当然,很多时候我们组件的封装是需要与业务设计相关,这样维护性能也会稍微好一些,这些前面也都有说到过。 358 | 359 | ### 10.4.4 可配置的功能 360 | 361 | 功能的配置,其实很多也与状态和展示的配置相关。但是我们有些与业务相关的功能,则可以结合展示、功能来定义这样的配置。 362 | 363 | 举个例子,我们的这个卡片可以是视频、图片、文字三者其中之一的卡片: 364 | 365 | - 视频:点击播放 366 | - 图片:点击新窗口查看 367 | - 文字:点击无效果 368 | 369 | 这种时候,我们可以两种方式: 370 | (1) 每个功能模块自己控制,同时通过配置控制哪个功能模块的展示。 371 | (2) 模块展示会有些耦合,但在点击事件里,根据配置来进行不同的事件处理,获取不同的效果。 372 | 373 | 对应维护性和可读性来说,第一种方式会获得更好的效果。如果问什么情况下会用到第二种,大概是同样的呈现效果,在不同场景下的逻辑功能不一样时,使用会比较方便。 374 | 375 | 功能配置化这块就不过多描述了,毕竟这块需要与业务场景密切结合,后面[《第13章 实战:表单配置化实现》](./13.md)也有介绍。大家更多地可以思考下,自己的项目中,是否可以有调整的空间,来使得整体的项目更好维护呢? 376 | -------------------------------------------------------------------------------- /docs/vue-ebook/11.md: -------------------------------------------------------------------------------- 1 | # 第11章 全局数据管理与 Vuex 2 | 3 | > 本章节相关代码存放在[Github](https://github.com/godbasin/vue-ebook/tree/vue-sourcecode/11)中。 4 | 5 | 通常来说,我们刚开始一个项目的时候,项目规模都比较少。这样的情况下,我们并不会引入过于复杂的数据和状态管理工具,而随着我们项目在不断地变大、各个页面中需要共享和通信的内容越来越多,同时也加入了新成员、不同开发之间的习惯不一致,于是到后期可能会出现事件的传递、全局数据满天飞的情况。 6 | 7 | 这样会有什么隐患呢?大概就是当我们想要改某个全局数据的时候,发现影响到了其他的页面。更让人头疼的是在测试的时候并没有发现(毕竟有在项目规模较大的情况下,回归测试的成本也会越来越重),然后触发了线上事故。 8 | 9 | 当然,即使在这样的情况下,也并不意味着我们需要在一开始就需要使用很完善的工具库。除非是项目在最初就已经定位为较大规模,这种情况下我们需要根据预期来选型。否则项目在不断维护和壮大的过程中,我们需要不断地进行优化甚至重构,同时根据需要引进不同的工具库来进行管理。 10 | 11 | 我们来看一下,一般全局数据要用怎样的方式来管理会比较好。 12 | 13 | ## 11.1 组件间通信 14 | 15 | 在 Vue 中,不管是页面也好,是模块或是公共组件也好,相互之间的通信几乎是必不可少的。我们先来看一下前面讲过的一些数据通信的方式。 16 | 17 | ### 11.1.1 父子组件通信 18 | 19 | [第4章](./4.md)中,我们其实也已经简单介绍过父子组件之间的通信,一般来说会通过 Prop 从父组件往子组件传递数据,同时子组件可以通过`$emit()`把数据传递到父组件。除此之外,父组件还可以通过`ref`来引用和访问子组件。同样的,还可以使用`$parent`、`$children`、`$root`等 API 来分别获取父实例、子实例和根实例。 20 | 21 | 那么,除了父子以外的组件要怎么处理呢? 22 | 23 | ### 11.1.2 全局事件管理 24 | 25 | 除了不断地使用`$parent`、`$children`、`$root`等 API 来叠加使用获取想要的实例(不推荐这样的操作,因为一旦布局调整就变得很被动了),我们可以在根实例上提供统一的事件处理,同时封装成公共库的方式来提供。这里直接使用[第8章](./8.md)中 Todo List 弹窗来作为例子,通过在最外层导出根实例: 26 | 27 | ```js 28 | // main.js 29 | // 默认 export 该 Vue 实例 30 | export default new Vue({ 31 | el: "#app", 32 | router, // 传入路由能力 33 | render: h => h(App) 34 | }); 35 | ``` 36 | 37 | 然后在需要的组件或者公共函数方法里引入,同时直接使用`$emit`触发事件、`$on`监听事件: 38 | 39 | ```js 40 | // confirm.js 41 | // 获取该实例 42 | import vm from "../main"; 43 | // 传入标题、内容、确认按钮和取消按钮的文案 44 | export function confirmDialog({ title, text, cancelText, confirmText }) { 45 | return new Promise((resolve, reject) => { 46 | // 把 reject 和 resolve 通过 $emit 事件传参带过去,方便进行 Promise 状态扭转 47 | vm.$emit("setDialog", { 48 | title, 49 | text, 50 | cancelText, 51 | confirmText, 52 | resolve, 53 | reject 54 | }); 55 | }); 56 | } 57 | 58 | // App.vue 59 | import vm from "../main"; 60 | export default { 61 | // 其他选型省略 62 | mounted() { 63 | this.$nextTick(() => { 64 | vm.$on("setDialog", dialogInfo => { 65 | // 将弹窗相关信息、弹窗组件添加进 component 数组中 66 | this.items.push({ dialogInfo, component: ConfirmDialog }); 67 | }); 68 | }); 69 | } 70 | }; 71 | ``` 72 | 73 | > [点击此处查看页面效果](https://vue-eboook-1255459943.cos.ap-chengdu.myqcloud.com/11/1-event-bus/index.html) 74 | > [点击此处查看源码](https://github.com/godbasin/vue-ebook/tree/vue-sourcecode/11/1-event-bus) 75 | 76 | 通过这样的方式,我们可以在每个需要的组件里,引入根实例,然后再在需要的时候分别进行事件的监听和触发就可以了。但是这个方法的缺点在本章开头也有提到,我们在新增一个事件监听和触发的时候,很容易和已有的事件名冲突,同时我们想要查看到底有哪些地方进行了某个事件的监听,也只能通过全局搜索的方式来查找,多人协作的情况下很容易出现问题。 77 | 78 | ## 11.2 全局数据管理 79 | 80 | 除了通过事件通知数据变更,常见的还有其他方式来管理全局数据,我们一起来看一下。 81 | 82 | ### 11.2.1 数据的流动 83 | 84 | [第10章](./10.md)我们介绍了如何将应用和界面抽象成数据管理,其中说到一个应用包含了静态的模板和从外部注入的数据(见 10.2 章节)。数据在注入到我们的应用中后,并不只是简单地存在,它可能会影响应用的状态、同时也会影响其他同样注入的数据。数据与数据之间的交互,其实在某方面相等于我们组件之间的通信,包括但不限于以下的一些方式: 85 | 86 | - 事件通知 87 | - 共享对象 88 | - 单方向流动 89 | 90 | #### 事件通知 91 | 92 | 事件的监听和触发机制,在许多的场景下都能适用,如浏览器点击、输入框的输入操作,则是典型的事件机制。而在很多时候,我们也可以通过事件通知的方式,来进行数据间的交互,如 Websocket 机制。在 Vue 中,除了使用根实例来绑定作为事件中心,更常见的就是我们自行创建一个事件中心来进行管理: 93 | 94 | ```js 95 | // eventBus.js 96 | const events = []; 97 | 98 | function on(eventName, callback) { 99 | let event = events.find(x => x.eventName === eventName); 100 | if (event) { 101 | // 如果已有该事件,添加到监听者中 102 | event.addListener(callback); 103 | } else { 104 | // 如果没有该事件,则添加新事件,并添加到监听者 105 | event = new MyEvent(eventName); 106 | event.addListener(callback); 107 | events.push(event); 108 | } 109 | } 110 | 111 | function emit(eventName, ...params) { 112 | let event = events.find(x => x.eventName === eventName); 113 | // 如果已有该事件,则触发 114 | if (event) { 115 | event.trigger(...params); 116 | } 117 | } 118 | 119 | class MyEvent { 120 | constructor(eventName) { 121 | // 创建一个事件,传入事件名 122 | this.eventName = eventName; 123 | // 同时动态生成一个监听者管理 124 | this.listeners = []; 125 | } 126 | // 触发事件,传入参数 127 | trigger(...params) { 128 | // 遍历监听者,触发回调 129 | this.listeners.forEach(x => { 130 | x(...params); 131 | }); 132 | } 133 | // 添加监听者 134 | addListener(callback) { 135 | this.listeners.push(callback); 136 | } 137 | } 138 | export default { 139 | on, 140 | emit 141 | }; 142 | ``` 143 | 144 | 同样的我们要实现前面 Todo List 的能力,只需要调整触发和监听的地方: 145 | 146 | ```js 147 | // confirm.js 148 | // 获取事件中心 149 | import eventBus from "./eventBus"; 150 | export function confirmDialog({ title, text, cancelText, confirmText }) { 151 | return new Promise((resolve, reject) => { 152 | // 把 reject 和 resolve 通过 emit 事件传参带过去,方便进行 Promise 状态扭转 153 | eventBus.emit("setDialog", { 154 | title, 155 | text, 156 | cancelText, 157 | confirmText, 158 | resolve, 159 | reject 160 | }); 161 | }); 162 | } 163 | 164 | // App.vue 165 | // 获取事件中心 166 | import eventBus from "./utils/eventBus"; 167 | export default { 168 | // 其他选型省略 169 | mounted() { 170 | this.$nextTick(() => { 171 | eventBus.on("setDialog", dialogInfo => { 172 | // 将弹窗相关信息、弹窗组件添加进 component 数组中 173 | this.items.push({ dialogInfo, component: ConfirmDialog }); 174 | }); 175 | }); 176 | } 177 | }; 178 | ``` 179 | 180 | 事件通知机制很方便,可以随意地定义触发的时机,也可以任意地点使用监听或是触发。但前面也说过,事件机制的弊端也是很明显,就是每一个事件的触发对应一个监听,关系是一一对应。在整个应用中看,则是散落在各处,随意乱窜的数据流动。需要定位的时候,只能通过全局搜索的方式来跟踪数据的去向。 181 | 182 | 我们来看个示例图(图 11-1),这里只有事件 1 和事件 2 的触发和监听,存在跨组件甚至跨页面的事件通知。如果在一个规模较大的应用中,可能会导致满屏的事件,同时事件之间并没有什么规律可循。这样我们每次改动事件相关的代码,例如多传一个参数、改变某个参数,都可能会导致未知的错误,需要全局搜索出相关的事件,并一一进行回归测试才可以。 183 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-11-2.jpg) 184 | 图 11-1 事件通知通信机制 185 | 186 | 当然,也有些人会定义一个中转站,所有的事件数据流都会经过那,这样的维护方式会有所改善。 187 | 188 | ![imgae](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-11-3.jpg) 189 | 图 11-2 事件通知增加中转站 190 | 191 | 类似这种,所有的数据流都可以在中转站找到,所有的数据都会储存在这里,数据的更新也必会经过这里。这样我们在新增一个事件的时候,就不会担心和已有的事件名冲突,同时也可以使用 Typescript 来定义每个事件的参数类型,就可以稍微避免一些改动引起不必要的错误了。 192 | 193 | #### 共享对象 194 | 195 | 共享对象是很简单的一种方式,当我们需要多个地方使用相同的数据,我们就把它们放置在一个地方,大家都去那理获取和更新。 196 | 197 | ![imgae](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-11-4.jpg) 198 | 图 11-3 共享对象通信机制 199 | 200 | 常见的方式是,我们可以简单的使用一个叫`globalData.js`的文件来管理全局用的数据,文件共享的方式,其实和前面说到的共享对象的方式比较相似: 201 | 202 | ```js 203 | // globalData.js 204 | // globalData 用来存全局数据 205 | let globalData = {}; 206 | 207 | // 获取全局数据 208 | // 传 key 获取对应的值 209 | // 不传 key 获取全部值 210 | export function getGlobalData(key) { 211 | return key ? globalData[key] : globalData; 212 | } 213 | 214 | // 设置全局数据 215 | export function setGlobalData(key, value) { 216 | // 需要传键值对 217 | if (key === undefined || value === undefined) { 218 | return; 219 | } 220 | globalData = { ...globalData, [key]: value }; 221 | return globalData; 222 | } 223 | 224 | // 清除全局数据 225 | // 传 key 清除对应的值 226 | // 不传 key 清除全部值 227 | export function clearGlobalData(key) { 228 | // 需要传键值对 229 | if (key === undefined) { 230 | globalData = {}; 231 | } else { 232 | delete globalData[key]; 233 | } 234 | return globalData; 235 | } 236 | ``` 237 | 238 | 使用这种方式的全局数据,是会在页面刷新之后丢失的。如果像用户的登录态信息这种需要缓存的,更好的方式是存到 cookie 或者缓存里。 239 | 240 | 通过注入对象的引用,来在不同组件中获取相同的数据源。在服务端开发的时候使用这样的方式,需要考虑锁的问题,当然单线程的 JS 里面这样的情况比较少,几乎可以不考虑。同时,很多时候我们定义了一个对象,某些地方想要共享这个对象包括它的状态数据,有些地方又想要获取一个全新初始化的对象。这种情况下,我们需要考虑怎样去维护一套这种数据与实例。 241 | 242 | #### 单方向流动 243 | 244 | 在全局数据的使用变频繁之后,我们在定位问题的时候还会遇到不知道这个数据为何改变的情况,因为所有引用到这个全局数据的地方都可能对它进行改变。这种情况下,给数据的流动一个方向,则可以方便地跟踪数据的来源和去处。通过流的方式来管理状态,常见的状态管理工具像 Vuex、Redux 等,都是这样管理的。 245 | 246 | ![imgae](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-11-5.jpg) 247 | 图 11-4 单方向流动通信机制 248 | 249 | 当然,赋予数据流方向,但是数据的存放呢,可以通过共享对象的方式,也可以维护一个数据中心。所有的数据变更方式、触发逻辑、影响范围,都在数据中心里面管理和维护。后面我们会介绍 Vuex,所以这里先不过多讲解单方向流动的数据管理实现。 250 | 251 | #### 树状作用域 252 | 253 | 很多时候,我们的应用通过一层层地封装和隔离,最终会呈现为树状。我们可以根据组件的树状作用域,结合共享对象的管理,来注入树状的数据结构。典型如 Angular 里,则是通过提供通用的依赖注入方式,配合树状的模块管理,可通过局部注入实例来获取共享或是隔离的数据。当然,这样的做法在其他框架中并不常见,而 Vue 中使用的更多是平级的单向数据流动,毕竟在 Angular 中树状结构的出现部分是为了优化脏检查机制,Vue 中我们只需要将数据变动更新到界面中就可以了。 254 | 255 | ## 11.3 使用 Vuex 256 | 257 | 前面我们讲到,在 Vue 组件中是通过`prop`属性和自定义事件`$on`、`$emit`可以实现父子组件间的通信。但很多时候我们的数据是需要跨组件、甚至跨页面进行共享,这种情况下我们需要更好的方式来维护这些全局的数据。我们已经讲了关于全局数据状态管理的一些常用方法,各种方式的优势和缺点也都有说到,这里我们就直接介绍 Vuex。 258 | 259 | Vuex 是官方推荐的状态管理工具,通过单向数据流的方式来维护状态变更。所有的数据都会存到 Store 里,只能通过提交 Mutation 或者触发 Action 的方式来改变数据,在这个过程中就会形成一个单向流动:`(Action) -> Mutation -> Store -> update view`。关于什么是 Mutation 和 Action,什么又是 Store,可以参考下官方的配图,后面会有详细介绍: 260 | 261 | ![imgae](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-11-6.png) 262 | 图 11-5 Vuex 状态更新 263 | 264 | 关于 Vuex 的用途,可以理解一下官方描述:它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。 265 | 266 | ### 11.3.1 Vuex 核心概念 267 | 268 | 关于 Vuex,有一些基本的核心概念需要先理解一下。 269 | 270 | #### State 271 | 272 | State 为状态,我们可以理解为上一章内容中抽象的数据。Vuex 中的 State,也可以理解为一个绑定到界面或组件的状态变量,就像是`data`中的变量一样: 273 | 274 | ```js 275 | new Vue({ 276 | // state 277 | data() { 278 | return { 279 | count: 0 280 | }; 281 | } 282 | }); 283 | ``` 284 | 285 | 而在 Vuex 中,State 通常是指全局的应用状态。因为 Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。这样所有 State 的集合,就是 Store。 286 | 287 | #### Store 288 | 289 | 每一个 Vuex 应用的核心就是仓库(Store),它包含着我们应用中大部分的状态(State)。状态也是数据的一种,相比直接展示在页面中的内容,状态更多时候是控制展示方式和逻辑的一些数据。像我们常说的用户登录态、地理位置等,一般来说我们存到 Store 中的数据,大多数都是需要多组件、多页面或是整个应用中共享的数据。 290 | 291 | 一个简单的 Store 是怎样注入到我们的应用中的呢,我们来看一下: 292 | 293 | ```js 294 | // 引入Vuex库 295 | Vue.use(Vuex); 296 | 297 | // 创建一个Store 298 | const store = new Vuex.Store({ 299 | // 设置状态 300 | state: { 301 | count: 0 302 | }, 303 | // Mutation用于更新状态 304 | mutations: { 305 | increment(state) { 306 | state.count++; 307 | } 308 | } 309 | }); 310 | 311 | // Vuex通过Store选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 ): 312 | const app = new Vue({ 313 | // 其他选项省略 314 | // 把store对象提供给“store”选项,这可以把store的实例注入所有的子组件 315 | store 316 | }); 317 | ``` 318 | 319 | 当然,我们也可以通过创建单文件或是全局对象的方式来维护这些全局的状态。但这样的方式中,数据的变化不能得到监听,需要使用方自行地去获取。我们还可以使用事件、`getter/setter`的方式来进行,但依然存在一个问题,数据的变化无法直接更新到组件和页面中。所以就有了 Vuex,使用 Vuex 我们可以获得以下效果: 320 | (1) 当 Vue 组件从 Store 中读取状态的时候,若 Store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。 321 | (2) 改变 Store 中的状态只能通过 Action 或者 Mutation,这样使得我们可以方便地跟踪每一个状态的变化,来定位具体问题。 322 | 323 | 每个应用将仅仅包含一个 Store 实例,单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。但如果每个应用只包含了一个 Store,我们要怎样避免在不断引入全局状态的时候,避免 Store 变得臃肿呢? 324 | 325 | #### Module 326 | 327 | 为了解决应用变得复杂时 Store 难以维护,Vuex 允许我们将 Store 分割成模块 Module。每个模块拥有自己的 State、Mutation、Action 等,其实相当于我们将一个 Store 单一对象分成多个对象来维护,但最终也会合并为一个来进行更新,这样即使是多个模块也能够对同一 Mutation 或 Action 作出响应。 328 | 329 | 那如果我们在不同的页面中,有相同的一些状态命名发生了冲突,要如何解决呢?我们还可以通过添加`namespaced: true`的方式,来创建带命名空间的模块,同时 Module 还支持嵌套使用: 330 | 331 | ```js 332 | const store = new Vuex.Store({ 333 | modules: { 334 | account: { 335 | namespaced: true, // 带命名空间 336 | // 模块内容(module assets) 337 | state: { ... }, // 模块内的状态已经是嵌套的了,使用namespaced属性不会对其产生影响 338 | // 可以拥有自己的Action和Mutation 339 | actions: { 340 | login () { ... } // -> dispatch('account/login') 341 | }, 342 | mutations: { 343 | login () { ... } // -> commit('account/login') 344 | }, 345 | // 嵌套模块 346 | modules: { 347 | // 继承父模块的命名空间 348 | myPage: { 349 | state: { ... } 350 | }, 351 | // 进一步嵌套命名空间 352 | posts: { 353 | namespaced: true, 354 | state: { ... } 355 | } 356 | } 357 | } 358 | } 359 | }) 360 | ``` 361 | 362 | #### Mutation 363 | 364 | 前面在 Store 的代码示例中也有出现过 Mutation,我们也说过,数据流的单向流动可以让开发者掌握所有数据状态的变更来源和去向。而 Mutation 的作用类似于一个守卫,所有的状态变更都必须来自 Mutation: 365 | 366 | ```js 367 | const store = new Vuex.Store({ 368 | state: { 369 | count: 1 370 | }, 371 | mutations: { 372 | // 每个 Mutation 都有一个字符串的事件类型 type 和一个回调函数 handler 373 | // 这个回调函数就是我们实际进行状态更改的地方 374 | increment(state) { 375 | // 变更状态 376 | state.count++; 377 | } 378 | } 379 | }); 380 | 381 | // 要调用 Mutation handler,你需要以相应的 type 调用 store.commit 方法 382 | store.commit("increment"); 383 | ``` 384 | 385 | 也就是说,在 Vuex 中状态的变更只能通过提交 Mutation 来更新状态,而提交 Mutation 也只能通过 commit 对应的的事件 type 来进行。在 Vuex 中,Mutation 都是同步函数,如果是异步的话,我们的状态变更就不实时,这会影响后续其他地方获取状态的实时性。那如果我们需要异步操作,例如从后台接口拉取的数据更新,这种情况下我们可以使用 Action。 386 | 387 | #### Action 388 | 389 | 相比与 Mutation,Action 的不同之处在于: 390 | (1) Action 提交的是 Mutation,而不是直接变更状态。 391 | (2) Action 可以包含任意异步操作。 392 | 393 | 我们来看一下 Action 的使用: 394 | 395 | ```js 396 | const store = new Vuex.Store({ 397 | state: { 398 | count: 0 399 | }, 400 | mutations: { 401 | increment(state) { 402 | state.count++; 403 | } 404 | }, 405 | actions: { 406 | // Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象 407 | increment(context) { 408 | // 因此可以调用 context.commit 提交一个 mutation 409 | context.commit("increment"); 410 | // 其他的,也可以通过 context.state 来获取 state 411 | } 412 | } 413 | }); 414 | ``` 415 | 416 | 关于 Vuex,还有 Getter 等概念,Getter 类似于 computed 之类的,Getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。这里就不过多讲解,大家可以去官网上补充更多的内容,我们来讲一些实战的用法吧。 417 | 418 | ### 11.3.2 Vuex 使用 419 | 420 | Vuex 使用单一状态树,即用一个对象就包含了全部的应用层级状态。Vuex 主要在什么时候用呢?当我们一些数据需要共享的时候,例如用户信息,可能会在多个组件中使用到的。一些 ajax 请求的数据,可通过 Action 来触发请求,完成后更新到 State,页面可直接更新。至于 Vuex 的使用方式和管理模式多种多样,如: 421 | (1) 根据 Vuex 的核心概念分文件管理,像把所有的 Action 放一起管理、Mutation 放一起。 422 | (2) 根据业务功能分文件管理,相似业务数据维护在一个 Store 中。 423 | 424 | #### 全局弹窗 Vuex 实现 425 | 426 | 前面在[第8章](./8.md)的时候,我们使用了在根实例上注册事件监听和触发的方式来实现全局弹窗,在本章介绍 Event Bus 的时候,也使用自制的事件管理来实现全局弹窗。现在,我们来使用 Vuex 重新实现一遍吧。 427 | 428 | **(1) 设计弹窗 Store。** 429 | 430 | 为了实现每次调用`confirmDialog`事件的时候,都新增一个弹窗,我们可以使用一个弹窗列表的方式来管理: 431 | 432 | ```js 433 | // dialogStore.js 434 | import Vue from "vue"; 435 | import Vuex from "vuex"; 436 | Vue.use(Vuex); 437 | 438 | const dialogStore = new Vuex.Store({ 439 | state: { 440 | // 弹窗列表,用来保存可能弹窗的一系列弹窗 441 | dialogList: [] 442 | }, 443 | mutations: { 444 | removeDialog(state, index) { 445 | // 移除弹窗 446 | state.dialogList.splice(index, 1); 447 | }, 448 | setDialog( 449 | state, 450 | { title, text, cancelText, confirmText, resolve, reject } 451 | ) { 452 | // 添加新的弹窗 453 | state.dialogList.push({ 454 | title, 455 | text, 456 | cancelText, 457 | confirmText, 458 | resolve, 459 | reject 460 | }); 461 | } 462 | } 463 | }); 464 | 465 | export default dialogStore; 466 | ``` 467 | 468 | **(2) 新增弹窗的方法。** 469 | 470 | 我们需要实现一个用来添加弹窗确认的方法,同时返回一个 Promise,当用户进行了操作之后,就更新 Promise 的状态: 471 | 472 | ```js 473 | // confirm.js 474 | import dialogStore from "../components/ConfirmDialog/dialogStore"; 475 | 476 | // 传入标题、内容、确认按钮和取消按钮的文案 477 | export function confirmDialog({ title, text, cancelText, confirmText }) { 478 | return new Promise((resolve, reject) => { 479 | // 调用 dialogStore.commit 提交 setDialog 480 | // 把 reject 和 resolve 通过事件传参带过去,方便进行 Promise 状态扭转 481 | dialogStore.commit("setDialog", { 482 | title, 483 | text, 484 | cancelText, 485 | confirmText, 486 | resolve, 487 | reject 488 | }); 489 | }); 490 | } 491 | 492 | export default confirmDialog; 493 | ``` 494 | 495 | **(3) 使用动态组件添加。** 496 | 497 | 和[第8章](./8.md)一样,我们可以使用动态组件的方式,来支持同时多个弹窗的交互确认: 498 | 499 | ```html 500 | 501 | 516 | 517 | 538 | ``` 539 | 540 | **(4) 弹窗数据绑定。** 541 | 542 | 因为通过列表的方式来管理弹窗的 Store,同时我们在动态组件中使用 Prop 的方式传入弹窗的信息,所以我们可以从 Prop 中获取到弹窗的内容: 543 | 544 | ```html 545 | 546 | 576 | 577 | 616 | ``` 617 | 618 | **(5) 弹窗确认。** 619 | 620 | 最后,我们就可以使用`confirmDialog`方法来进行弹窗确认: 621 | 622 | ```js 623 | // 传入弹窗的内容,并通过 then 和 catch 来获取用户的操作,以便继续后续的逻辑处理 624 | confirmDialog({ 625 | text: "确认要删除吗?" 626 | }) 627 | .then(res => { 628 | // 用户点击确认 629 | // 二次弹窗确认 630 | confirmDialog({ 631 | text: "真的确认要删除吗?" 632 | }) 633 | .then(res => { 634 | // 用户点击确认 635 | }) 636 | .catch(() => { 637 | // 用户点击取消 638 | }); 639 | }) 640 | .catch(() => { 641 | // 用户点击取消 642 | }); 643 | ``` 644 | 645 | 通过这种方式,我们可以进行二次弹窗,也可以进行多个弹窗叠加。同时使用 Promise 的方式,也可以更有连贯性地编写代码。 646 | 647 | > [点击此处查看页面效果](https://vue-eboook-1255459943.cos.ap-chengdu.myqcloud.com/11/2-vuex-demo/index.html) 648 | > [点击此处查看源码](https://github.com/godbasin/vue-ebook/tree/vue-sourcecode/11/2-vuex-demo) 649 | 650 | 关于 Vuex,还有更多的使用方式,这里只是展示了绑定弹窗的一个使用方式。而在大型项目的管理中,除了将 Store 模块化,我们还可以根据需要来对 Action 和 Mutation 进行管理。除此之外,我们也还是可以自行开发全局数据、事件管理等方式来进行。但为了避免多人协作的一些问题,一般我们在选型的时候,更多会考虑规范性和维护性,所以选用官方提供的一些库也是很好的选择。 651 | -------------------------------------------------------------------------------- /docs/vue-ebook/15.md: -------------------------------------------------------------------------------- 1 | # 第15章 Vue 周边拓展 2 | 3 | > 本章节相关代码存放在[Github](https://github.com/godbasin/vue-ebook/tree/vue-sourcecode/15)中。 4 | 5 | Vue 生态如今越来越完善了,随之而来也有了不少的周边。除了前面的 Vue Loader、Vuex、Vue CLI 等常用工具,这里也介绍一些可以使用 Vue 进行开发的周边拓展工具,包括 Vuepress、Vue 生成小程序等。 6 | 7 | ## 15.1 VuePress 快速搭建文档平台 8 | 9 | VuePress 是 Vue 驱动的静态网站生成工具,基于 Markdown 维护的文档网站。如果你有自己的博客,那应该也了解过 Hexo 工具,而 VuePress 可以理解为一个 Vue 版本的 Hexo。基于 Vue 和 Webpack 的开发方式,可以让开发者简单又便捷地实现一些想要的功能。 10 | 11 | ### 15.1.1 VuePress 介绍 12 | 13 | 一个 VuePress 网站其实是一个由 Vue、Vue Router 和 Webpack 驱动的单页应用。虽然和 Nuxt.js 很相似,但相比于 Nuxt.js 用于应用程序的构建,VuePress 专注于以内容为中心的静态网站。VuePress 由两部分组成: 14 | (1) 由 Vue 驱动的极简静态网站生成器。我们在初始化项目之后,只需要添加 Markdown 文档,以及进行简单的配置就可以生成一个功能完备的静态网站(包括菜单、导航、路由、搜索等功能)。 15 | (2) 提供基于技术文档的默认主题(可以参考 Vue 官方主题样式),同时也支持自定义配置主题方式。 16 | 17 | 对比 Hexo,VuePress 最大的亮点在于,除了通用的 Markdown 能力之外,VuePress 允许在 Markdown 中使用 Vue,也就是说我们写着 Markdown 的过程中可以插入一个 Vue 组件。而基于 Vue 的驱动方式,我们可以使用 Vue 自由地调整默认主题和样式,对于熟悉 Vue 开发的小伙伴来说,开发效率有了质的提升。 18 | 19 | ### 15.1.2 快速开发静态网站 20 | 21 | 介绍了那么多,我们来看一下使用 VuePress 生成一个静态网站到底有多简单。 22 | 23 | #### 安装和使用 24 | 25 | 首先我们需要安装依赖,完整的开发和构建流程步骤如下。 26 | 27 | **(1) 安装依赖。** 28 | 29 | ```cmd 30 | # 全局安装 vuepress 31 | npm install -g vuepress 32 | 33 | # 安装依赖 34 | npm install 35 | ``` 36 | 37 | **(2) 目录结构设计。** 38 | 为了方便管理,我们创建一个 docs 文件夹,将一些 markdown 文档放在里面维护,文件夹外面就可以做一些简单的依赖管理、脚本等。整体来说,我们的目录结构如下: 39 | 40 | ```cmd 41 | ├─docs/ // vuepress根目录 42 | │ ├─.vuepress/ // vuepress应用相关 43 | │ │ ├─dist/ // 编译之后的静态文件 44 | │ │ ├─theme/ // 自定义主题 45 | │ │ ├─config.js // 配置 46 | │ │ ├─enhanceApp.js // 应用级别的配置 47 | │ ├─basic/ // 基础类文档 48 | │ ├─vue/ // Vue类文档 49 | │ ├─README.md // 首页文档和配置 50 | │ 51 | ├─deploy.sh // 部署到Github脚本 52 | ├─package.json // 项目配置 53 | ├─README.md // 项目说明 54 | ``` 55 | 56 | VuePress 应用相关的配置都放置在.vuepress 文件夹下,关于自定义主题、配置和应用级别的配置后面会一点点描述。其他的文档归类可根据文档类别进行文件夹的区分,而 docs 下的默认 README.md 则是首页内容。 57 | 58 | 基于这样的目录结构,我们可以确定如何进行开发和打包,因此可以在 package.json 配置: 59 | 60 | ```json 61 | { 62 | "scripts": { 63 | "docs:dev": "vuepress dev docs", 64 | "docs:build": "vuepress build docs" 65 | } 66 | } 67 | ``` 68 | 69 | 然后我们可以通过命令来进行: 70 | 71 | ```cmd 72 | # 本地开始写作 73 | npm run docs:dev 74 | 75 | # 构建生成静态文件 76 | npm run docs:build 77 | ``` 78 | 79 | **(3) 文档内容管理填充。** 80 | 这里我们分了基础类文档和 Vue 类文档,分别放置在`docs/basic`和`docs/vue`目录下面,在将 markdown 挪到对应的地方之后,我们可以这么进行组织: 81 | 82 | ```cmd 83 | ├─docs/ // vuepress根目录 84 | │ ├─.... // 其他省略 85 | │ ├─basic/ // 基础类文档 86 | │ │ ├─1/ // 基础类文档分类1 87 | │ │ ├─2/ // 基础类文档分类2 88 | │ │ ├─README.md // 基础类文档默认页 89 | │ ├─vue/ // Vue类文档 90 | │ │ ├─1/ // Vue类文档分类1 91 | │ │ ├─2/ // Vue类文档分类2 92 | │ │ ├─README.md // Vue类文档默认页 93 | │ ├─README.md // 首页文档和配置 94 | │ 95 | ├─.... // 其他省略 96 | ``` 97 | 98 | 调整好内容之后,我们来看看要怎么配置。 99 | 100 | **(4) 首页配置。** 101 | 默认的主题提供了一个首页(Homepage)的布局这里,我们直接使用它,所以在根级的 README 中进行以下配置: 102 | 103 | ```md 104 | ## 105 | 106 | home: true # 使用默认主题的首页布局 107 | actionText: 快速阅读 → 108 | actionLink: /basic/1/basic-1-1.md 109 | features: 110 | 111 | - title: 基础类文档 112 | details: 基础类文档描述。 113 | - title: Vue 类文档 114 | details: Vue 类文档描述。 115 | - title: 其他补充 116 | details: 其他补充描述。 117 | footer: MIT Licensed | Copyright © 2018-present 被删 118 | 119 | --- 120 | ``` 121 | 122 | **(5) 默认配置。** 123 | 想要项目顺利跑起来,我们还需要补充默认配置,需要在 docs/.express 文件夹下添加 config.js 文件: 124 | 125 | ```js 126 | // docs/.express/config.js 127 | module.exports = { 128 | title: "某个前端文档网站", // 首页标题 129 | shouldPrefetch: () => false, // 关闭预加载,可能导致首页渲染比较慢 130 | description: "Just playing around", // 首页描述 131 | themeConfig: { 132 | // logo: '/assets/img/logo.png', 133 | // 假定是 GitHub. 同时也可以是一个完整的 GitLab URL 134 | repo: "godbasin/vuepress-demo", 135 | // 自定义仓库链接文字。默认从 `themeConfig.repo` 中自动推断为 136 | // "GitHub"/"GitLab"/"Bitbucket" 其中之一,或是 "Source"。 137 | repoLabel: "Github", 138 | // 假如你的文档仓库和项目本身不在一个仓库: 139 | // docsRepo: 'vuejs/vuepress', 140 | // 假如文档不是放在仓库的根目录下: 141 | // 这里我们是放置在docs下的 142 | docsDir: "docs", 143 | // 假如文档放在一个特定的分支下: 144 | // 这里我们放在一个叫sourcecode的分支下,因为master需要用来放置生成的静态文件 145 | docsBranch: "sourcecode", 146 | // 顶部导航配置 147 | nav: [ 148 | { text: "概述", link: "/" }, 149 | { text: "基础类文档", link: "/basic/" }, 150 | { text: "Vue类文档", link: "/vue/" } 151 | // 我们也可以添加FAQ的 152 | // { text: 'FAQ', link: '/faq' } 153 | ], 154 | // 左侧导航菜单配置 155 | sidebar: { 156 | "/basic/": [ 157 | { 158 | title: "基础类文档分类1", // 菜单名 159 | collapsable: true, // 是否支持折叠菜单 160 | children: ["/basic/1/basic-1-1.md", "/basic/1/basic-1-2.md"] 161 | }, 162 | { 163 | title: "基础类文档分类2", // 菜单名 164 | collapsable: true, // 是否支持折叠菜单 165 | children: ["/basic/2/basic-2-1.md", "/basic/2/basic-2-2.md"] 166 | } 167 | ], 168 | "/vue/": [ 169 | { 170 | title: "Vue类文档分类1", // 菜单名 171 | collapsable: true, // 是否支持折叠菜单 172 | children: ["/vue/1/vue-1-1.md", "/vue/1/vue-1-2.md"] 173 | }, 174 | { 175 | title: "Vue类文档分类2", // 菜单名 176 | collapsable: true, // 是否支持折叠菜单 177 | children: ["/vue/2/vue-2-1.md", "/vue/2/vue-2-2.md"] 178 | } 179 | ] 180 | } 181 | } 182 | }; 183 | ``` 184 | 185 | **(6) 启动项目。** 186 | 配置完成后,我们可以使用`npm run docs:dev`来启动本地环境,能看到环境搭建起来了: 187 | 188 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-15-1.jpg) 189 | 图 15-1 本地环境启动 190 | 191 | 然后我们可以看到页面效果: 192 | ![首页](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-15-2.jpg) 193 | 图 15-2 首页效果 194 | 195 | ![Vue类文档默认页](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-15-3.jpg) 196 | 图 15-3 Vue 类文档默认页效果 197 | 198 | 到这里,我们可以快速简单地将文档管理起来。 199 | 200 | #### 添加组件使用 201 | 202 | 接下来我们需要添加自己的组件,例如我想在首页中添加一个弹窗。 203 | 204 | **(1) 全局引入 Element。** 205 | 这里我想要直接使用 Element 的弹窗样式,可以通过引入 Element 来直接使用,首先我们需要执行`npm install element-ui`来安装 Element。同时,我们可以通过创建一个`.vuepress/enhanceApp.js`文件来做一些应用级别的配置: 206 | 207 | ```js 208 | // docs/.vuepress/enhanceApp.js 209 | // 当该文件存在的时候,会被导入到应用内部 210 | 211 | // 引入ElementUI 212 | import ElementUI from "element-ui"; 213 | // 全局引入样式 214 | import "element-ui/lib/theme-chalk/index.css"; 215 | 216 | export default ({ 217 | Vue // VuePress 正在使用的 Vue 构造函数 218 | }) => { 219 | try { 220 | // 使用element-ui 221 | Vue.use(ElementUI); 222 | } catch (e) { 223 | console.error(e.message); 224 | } 225 | }; 226 | ``` 227 | 228 | 到这里,我们已经全局引入了 ElementUI 库,接下来可以直接在文档中使用了。 229 | 230 | **(2) 在 Markdown 文档中使用 Vue 组件。** 231 | 一般来说,所有在`.vuepress/components`中找到的\*.vue 文件将会自动地被注册为全局的异步组件,例如这里我们增加一个 Element 的弹窗: 232 | 233 | ```html 234 | 235 | 236 | 239 | 240 | 257 | ``` 258 | 259 | 然后我们可以直接使用这些组件在任意的 Markdown 文件中(组件名是通过文件名取到的),例如这里我们在首页中使用: 260 | 261 | ```md 262 | ## 263 | 264 | home: true 265 | actionText: 快速阅读 → 266 | actionLink: /basic/1/basic-1-1.md 267 | features: 268 | 269 | - title: 基础类文档 270 | details: 基础类文档描述。 271 | - title: Vue 类文档 272 | details: Vue 类文档描述。 273 | - title: 其他补充 274 | details: 其他补充描述。 275 | footer: MIT Licensed | Copyright © 2018-present 被删 276 | 277 | --- 278 | 279 | # Element 弹窗 280 | 281 | 282 | ``` 283 | 284 | 然后我们就可以看到效果: 285 | ![首页](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-15-4.jpg) 286 | 图 15-4 首页效果 287 | 288 | ![点击后出现弹窗](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-15-5.jpg) 289 | 图 15-5 点击后出现弹窗 290 | 291 | > [点击此处查看源码](https://github.com/godbasin/vue-ebook/tree/vue-sourcecode/15/2-vuepress) 292 | 293 | 对于 VuePress,快速生成可用主题和配置是它的一大亮点,而直接在 Markdown 文件中使用 Vue 组件则是特别便利实用的另外一个特色。当然,VuePress 还支持自定义主题配置,不过这里篇幅关系就不多介绍,大家感兴趣可以去官网查看,也可以去https://github.com/godbasin/front-end-playground查看使用VuePress搭建的前端游乐场,源码在sourcecode分支。 294 | 295 | ## 15.2 Vue 生成 web 与小程序 296 | 297 | 如今,小程序用完即走、无需安装的便捷吸引了越来越多的用户愿意使用,同时微信提供的流量吸引了不少的小程序开发者加入其中。小程序有一套自己的规范,与常用的几大框架都不好进行兼容,我们来看一下为什么。 298 | 299 | ### 15.2.1 小程序与 Web 的区别 300 | 301 | 小程序的渲染层和逻辑层分别由两个线程管理:渲染层的界面使用了 WebView 进行渲染,逻辑层采用 JsCore 线程运行 JS 脚本。通过双线程的方式,我们可以阻止开发者使用一些浏览器提供的,诸如跳转页面、操作 DOM、动态执行脚本的开放性接口,来保证小程序的安全性,也方便平台进行管控。但是这样的设计也带来其他的问题,逻辑层和渲染层的通信(`setData`)需要由 Native(微信客户端)做中转,逻辑层发送网络请求也经由 Native 转发。以上这些情况意味着: 302 | (1) 在浏览器中可以运行的 DOM、BOM 等对象和 API,都无法在小程序中使用。 303 | (2) 小程序的一些 API 使用方式与浏览器不一致(请求、缓存等)。 304 | 305 | 对于强交互的场景(通信过多容易引起性能问题),小程序引入了原生组件,这样用户和原生组件的交互可以节省通信,从而提升性能。但这样还意味着: 306 | (3) 在浏览器中可以使用的组件,在小程序中成了原生组件(input、video、canvas 等),使用方式和渲染效果完全不一致。 307 | 308 | 在这样的种种情况下,我们跑在浏览器中的代码,如果想要在小程序中运行,我们必须要重新开发来实现。那为什么一定要使用小程序呢,直接用 H5 不好吗?其实小程序做了很多的体验优化,同时天然的预加载、原生组件的流畅性,最重要的还是微信平台提供的流量,在很多情况下都不失为一个不错的选择。 309 | 310 | ### 15.2.2 开源类 Vue 小程序框架 311 | 312 | 虽然我们没法一套代码同时运行在浏览器和小程序中,但是依然有不少的开发者或者团队开发了一些框架,来尽可能使用一套代码完成多种环境的开发,例如 mpvue(Vue 系)、wepy(Vue 系)、taro(React 系)等。但毕竟不是官方支持,同时小程序的 API 和兼容性也不断在变动,使用的开源框架要是更新不及时,甚至可能成为技术债务。 313 | 314 | ### 15.2.3 kbone 的出现 315 | 316 | kbone 作为一套解决方案应运而生,用于支持让一个 vue 项目可以同时在 web 端和小程序端被使用。前面说过,在小程序中无法直接使用 DOM/BOM 等 API,因此 kbone 提供了适配层的方式来进行兼容。适配层它提供基础的 DOM/BOM API,让小程序端尽量能使用 web 端的能力: 317 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-15-1.png) 318 | 图 15-6 kbone 适配层 319 | 320 | 简单来说,就是提供了一个将 DOM/BOM 相关 API 转换成在小程序中的实现方式,这里有点像前面[《第13章 实战:表单配置化实现》](./13.md)中使用的配置方式,适配层维护一套虚拟 DOM 节点,然后根据节点信息,再通过简单的`wx:for`和`wx:if`来生成页面相关代码。 321 | 322 | 我们来看下,我们在[第8章](./8.md)中的 Todo List 代码,直接使用 kbone 来运行,跑在 web 和小程序两端的效果是怎样的。 323 | 324 | #### kbone 安装 325 | 326 | 使用 kbone,官方提供了三种方式。 327 | 328 | **(1) 新项目使用 kbone-cli。** 329 | 对于新项目,可以使用 kbone-cli 来创建项目,首先需要全局安装: 330 | 331 | ```cmd 332 | # 全局安装 kbone-cli 333 | npm install -g kbone-cli 334 | ``` 335 | 336 | **(2) 基于模板快速调整开发。** 337 | 除了使用 kbone-cli 外,也可以直接将现有模板复制下来,然后在模板基础上进行开发改造。 338 | 339 | **(3) 自行改造 Webpacl 配置开发。** 340 | 如果想要更灵活地搭建自己的项目,或者是想对已有的项目进行改造,则需要自己补充对应配置(构建到小程序代码的 webpack 配置、使用 webpack 构建中使用到的特殊插件 mp-webpack-plugin 配置)来实现 kbone 项目的构建。 341 | 342 | #### Todo List 接入 kbone 343 | 344 | 其实针对已有的项目,不管是以上哪种方式都是可以的,这里我们就直接使用 kbone-cli 生成一个新项目,然后把原有的 Todo List 内容导入查看。 345 | 346 | **(1) 初始化项目。** 347 | 首先执行`kbone init kbone-todo-list`,生成一个初始化项目,初始化的时候需要选择对应的框架,这里我们选择 Vue: 348 | 349 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-15-6.jpg) 350 | 图 15-7 kbone 初始化 351 | 352 | 初始化完成后,我们来看看目录说明: 353 | 354 | ```cmd 355 | 356 | 357 | ├─ build 358 | │ ├─ miniprogram.config.js // mp-webpack-plugin 配置 359 | │ ├─ webpack.base.config.js // Web 端构建基础配置 360 | │ ├─ webpack.dev.config.js // Web 端构建开发环境配置 361 | │ ├─ webpack.mp.config.js // 小程序端构建配置 362 | │ └─ webpack.prod.config.js // Web 端构建生产环境配置 363 | ├─ dist 364 | │ ├─ mp // 小程序端目标代码目录,使用微信开发者工具打开,用于生产环境 365 | │ └─ web // web 端编译出的文件,用于生产环境 366 | ├─ src 367 | │ ├─ common // 通用组件 368 | │ ├─ mp // 小程序端入口目录 369 | │ │ ├─ home // 小程序端 home 页面 370 | │ │ │ └─ main.mp.js // 小程序端入口文件 371 | │ │ └─ other // 小程序端 other 页面 372 | │ │ └─ main.mp.js // 小程序端入口文件 373 | │ ├─ detail // detail 页面 374 | │ ├─ home // home 页面 375 | │ ├─ list // list 页面 376 | │ ├─ router // vue-router 路由定义 377 | │ ├─ store // vuex 相关目录 378 | │ ├─ App.vue // Web 端入口主视图 379 | │ └─ main.js // Web 端入口文件 380 | └─ index.html // Web 端入口模板 381 | ``` 382 | 383 | **(2) 迁移 Todo List 代码。** 384 | 初始化 kbone 项目之后,我们需要将原有的代码迁移过来,迁移过程中有以下注意事项: 385 | 386 | - https 引入的 css/js 资源在小程序中不再可用,可以下载下来通过相对路径引入 387 | - scoped 样式在小程序中不可用,需要自行加作用域或是进行冲突解决 388 | - 部分样式在 Web 端和小程序端表现不一致,需要进行调整 389 | - Web 端的鼠标 hover、dbclick 等状态和事件在小程序中不再可用,需要进行调整 390 | - 之前全局引入了 Velocity 进行延时动画,在小程序中需要移除 391 | - 路由调整,需要同时调整`src/router/index.js`文件、`src/mp/todo/main.mp.js`文件、`build/miniprogram.config.js`文件、`build/webpack.mp.config.js` 392 | 393 | **(3) Web 端启动。** 394 | 代码迁移过程比较琐碎,这里就不详细讲啦,大家可以去找源码进行参考。迁移完之后,我们首先将 Web 端跑起来,需要执行`npm run web`命令之后,我们来看看效果: 395 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-15-8.png) 396 | 图 15-8 Web 端启动效果 397 | 398 | 我们能看到主要包括两个变动: 399 | (1) 增加添加按钮,因为在小程序端无法监控`@keyup.enter`事件。 400 | (2) 删除图标,从之前需要鼠标 hover 可见,调整成直接可见,因为在小程序中没有 hover 状态。 401 | 402 | 点击删除的时候,会进行弹窗,这里使用的是 Vuex 进行状态更新(参考[《第11章 全局数据管理与 Vuex》](./11.md)): 403 | ![点击删除](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-15-9.jpg) 404 | 图 15-9 点击删除弹窗效果 405 | 406 | **(4) 小程序端启动。** 407 | 在小程序端启动,要先执行`npm run mp`命令,接下来还需要先进入 dist/mp 目录执行`npm install`安装相关的依赖包(主要是 miniprogram-render 和 miniprogram-element),然后用开发者工具打开 dist/mp 目录后再进行 npm 构建(菜单栏-工具-构建 npm): 408 | 409 | ![工具中效果](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-15-10.jpg) 410 | 图 15-10 工具中效果 411 | 412 | 我们能看到除了样式有些不一致之外,其他功能都是可用的。另外,我们从工具中可以看到,最终生成的代码基本上是通过``组件来实现的,我们看看生成的代码: 413 | 414 | ```html 415 | 416 | 423 | 424 | ``` 425 | 426 | ```json 427 | // dist/mp/pages/todo/index.json 428 | // 这里使用了官方兼容层组件miniprogram-element 429 | { 430 | "enablePullDownRefresh": false, 431 | "usingComponents": { 432 | "element": "miniprogram-element" 433 | } 434 | } 435 | ``` 436 | 437 | 如果要继续看下去,你会发现 dist/mp/pages/todo/index.js 文件中也是很多兼容性的代码,也可以验证了我们前面说过的,适配层维护一套虚拟 DOM 节点信息,再通过简单的`wx:for`和`wx:if`配合 miniprogram-element 中的 element 组件来生成页面相关代码。 438 | 439 | 点击删除的时候,Vuex 状态相关也是可以正常执行的: 440 | ![手机执行效果](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-15-11.jpg) 441 | 图 15-11 手机执行效果 442 | 443 | > [点击此处查看源码](https://github.com/godbasin/vue-ebook/tree/vue-sourcecode/15/3-kbone-todo-list) 444 | 445 | 当然,kbone 也会存在一些问题,官方也进行了相关的说明,常见的例如: 446 | 447 | - 小程序里的所有页面必须同源 448 | - 受限于小程序环境,目前无法支持异步组件 449 | - 小程序不支持属性选择器,所以无法支持 scoped style 450 | - iframe 标签无法支持 451 | - vue 的 transition 组件无法完美支持 452 | - 不可使用 rem 453 | - 等等 454 | 455 | 关于 kbone 相关的暂时就介绍到这里,其实我们的 Todo List 样式是 PC 端适应的,而如果做了移动端的样式兼容,就可以很方便地使用 kbone 同构生成 Web 代码(移动端+PC)和小程序代码了,而小程序官方的微信开放社区也正是这样做的,一套代码维护多端。 456 | 457 | ## 15.3 其他 Vue 周边 458 | 459 | 除了 VuePress 和 kbone 之外,常用的还有 Nuxt.js。 460 | 461 | Nuxt.js 是一个基于 Vue(Vue.js、Vue Router、Vuex 等)、Webpack 和 Babel 的通用应用框架,它预设了利用 Vue.js 开发服务端渲染的应用所需要的各种配置,开发中可以直接基于初始化的项目上进行快速开发。使用服务器端渲染,最主要其实就是为了解决 SEO 的问题,同时服务器端渲染还能解决的首屏加载速度的问题。Nuxt.js 为"客户端/服务端"这种典型的应用架构模式提供了许多有用的特性,包括强大的路由功能、异步数据加载、中间件支持、布局支持等。当然,除了服务端渲染,Nuxt.js 同时还支持单页应用(SPA)、生成静态站点等模式。 462 | 463 | 其实我们前面也介绍过 Vuex、Vue CLI 等,另外 Vue 还包括了 Vue Loader、vue-devtools 等工具,由于篇幅关系这里不介绍使用,大家可以去官网上学习下怎样使用。Vue 作为如今国内的主流框架之一,提供了很完备的生态和社区,越来越多的开发者也加入进来一起建设,周边拓展可见也会越来越丰富。 464 | -------------------------------------------------------------------------------- /docs/vue-ebook/2.md: -------------------------------------------------------------------------------- 1 | # 第2章 Vue 环境快速搭建 2 | 3 | > 本章节相关代码存放在[Github](https://github.com/godbasin/vue-ebook/tree/vue-sourcecode/2)中。 4 | 5 | 上一章我们介绍了 Vue 框架都解决了开发者的哪些痛点,而 Vue 的安装使用也是很容易上手的。一般来说,前端使用 Vue 框架有两种方式: 6 | (1) 简单的页面,可以通过` 31 | 32 | 33 |
{{ message }}
34 | 45 | 46 | 47 | ``` 48 | 49 | 打开浏览器,我们便能看到想要的效果: 50 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-2-1.jpg) 51 | 图 2-1 直接引入 CDN 使用 Vue 52 | 53 | > [点击此处查看页面效果](https://vue-eboook-1255459943.cos.ap-chengdu.myqcloud.com/2/1-import-cdn/1-import-cdn.html) 54 | > [点击此处查看源码](https://github.com/godbasin/vue-ebook/blob/vue-sourcecode/2/1-import-cdn) 55 | 56 | 浏览器在解析了 Vue 框架的代码并执行之后,我们通过`new Vue()`启动了一个 Vue 根实例,而`
{{ message }}
`中使用双括号`{{}}`绑定的`message`变量也被替换成了对应的值。之前读完第一章的你们肯定已经能理解这中间究竟发生了什么,而 Vue 中也有很多相关的概念包括实例、组件、模板语法、生命周期等,会在后面几章详细地描述和介绍。 57 | 58 | ### 2.1.2 在线代码编写 59 | 60 | 前端有很多在线编写的平台,例如 codepen、jsfiddle 等,有了这些平台,我们再也不用在本地生成个临时的 HTML 文件来编写代码了。我们可以直接在这些平台上写测试代码,例如我们这里可以直接使用 Vue: 61 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-2-2.jpg) 62 | 图 2-2 codepen 所见即所得编程 63 | 64 | 在线编写的好处是所见即所得,我们只需要引入需要的资源,然后编写好代码,下方就能直接看到代码的效果,还是很方便的。同时这些在线编程的网站也常常提供了代码片段保存和分享的功能,例如 Element UI 也将每一段的代码示例存到 codepen,开发者只需要打开对应的链接就可以看到效果,同时还可以很方便地基于现有的逻辑来快速调试,是特别好用的一个工具。 65 | 66 | ## 2.2 Vue CLI 脚手架 67 | 68 | 脚手架在前端中也是频繁会出现的一个词,尤其近几年前端领域经历了翻天覆地的变化,前端开发页面更经常会使用到脚手架。脚手架来自于现实生活中的房屋工程,为了保证各施工过程顺利进行而搭建的的工作平台。在前端领域中,脚手架的功能也是相似的,为了保证开发过程的顺利、提供便利设施而搭建的开发环境。有了脚手架,我们可以通过简单的命令就能快速生成 Demo 代码、构建本地测试环境、编译和打包代码、发布到现网等等功能。 69 | 70 | 使用 Vue 框架,脚手架一般会优先选择官方提供的 Vue CLI。如果是熟悉 Webpack 或者 Gulp 的小伙伴,当然也可以自行搭建脚手架。Vue CLI 其实也是基于 Webpack 封装的便捷脚手架,所以接下来我们会先介绍如何使用该脚手架,再对比 Webpcak 看看官方 CLI 都封装了哪些能力。 71 | 72 | ### 2.2.1 快速创建项目 73 | 74 | 通常来说,脚手架可以让你快速地生成示例代码、搭建本地环境,也可以更新依赖的版本等,避免了每个开发者自行调整开发环境、打包逻辑等配置。Vue cli 也提供了这样的能力:对 Babel、TypeScript、ESLint、PostCSS、PWA、单元测试和 End-to-end 测试提供开箱即用的支持。 75 | 76 | #### Vue CLI 77 | 78 | Vue CLI 致力于将 Vue 生态中的工具基础标准化。它确保了各种构建工具能够基于智能的默认配置即可平稳衔接,这样你可以专注在撰写应用上,而不必花好几天去纠结配置的问题。使用方式很简单: 79 | 80 | ```cmd 81 | // 安装脚手架 82 | npm install -g @vue/cli 83 | // 脚手架生成 vue 项目,同时会自动安装依赖 84 | vue create vue-cli-demo 85 | ``` 86 | 87 | 生成之后的代码目录是这样的: 88 | ![](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-cli-demo-files.jpg) 89 | 图 2-3 Vue CLI 生成的项目 Demo 90 | 91 | > [点击此处查看页面效果](https://vue-eboook-1255459943.cos.ap-chengdu.myqcloud.com/2/2-vue-cli-service/index.html) 92 | > [点击此处查看源码](https://github.com/godbasin/vue-ebook/tree/vue-sourcecode/2/2-vue-cli-service) 93 | 94 | #### 启动项目 95 | 96 | 一般来说,比较规范的项目都会有个 README.md 文件,我们可以通过该文件看到项目相关的一些内容,包括项目背景、项目启动和构建、相关负责人等说明。在这里,我们使用 Vue CLI 生成的项目 Demo 也有一个 README.md 文件。打开这个文件,我们能看到一些简单的说明: 97 | 98 | ```cmd 99 | # Project setup 100 | yarn install 101 | 102 | # Compiles and hot-reloads for development 103 | yarn run serve 104 | 105 | # Compiles and minifies for production 106 | yarn run build 107 | 108 | # Run your tests 109 | yarn run test 110 | 111 | # Lints and fixes files 112 | yarn run lint 113 | ``` 114 | 115 | yarn 跟 npm 都是差不多的包管理器,区别在于 yarn 在安装时会速度更快(并行、离线等),以及版本统一管理的比较好。但如果你不是有特殊的喜好或者习惯,其实两个都可以用,例如这里的`yarn run serve`也可以用`npm run serve`来运行。 116 | 117 | 如果有些习惯不好的项目缺了 README,这种时候要怎么去启动一个项目呢?可以查看`package.json`文件: 118 | 119 | ```json 120 | { 121 | "scripts": { 122 | "serve": "vue-cli-service serve", 123 | "build": "vue-cli-service build", 124 | "lint": "vue-cli-service lint" 125 | } 126 | } 127 | ``` 128 | 129 | 一般来说,开发环境是`dev`、`serve`等,生产环境是`build`,`scripts`里是一些任务,运行命令`npm run taskName`就可以启动了。 130 | 131 | ![](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-cli-demo-serve.jpg) 132 | 图 2-4 Vue CLI 启动本地开发 133 | 134 | 图 2-4 中,我们可以看到任务已经启动了,访问输出的地址(这里是`http://localhost:8080/`或者`http://10.40.120.53:8080/`)就能看到我们的项目跑起来了。 135 | 136 | ![](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-cli-demo-page.jpg) 137 | 图 2-5 Vue CLI 生成的 Demo 效果 138 | 139 | ### 2.2.2 CLI 与 Webpack 介绍 140 | 141 | 前面其实也有简单说到 Vue CLI 和 webpack 的关系,这里我们更准确地描述一下。Vue CLI 服务是构建于 webpack 和 webpack-dev-server 之上的,它包含了: 142 | 143 | - 加载其它 CLI 插件的核心服务 144 | - 一个针对绝大部分应用优化过的内部的 webpack 配置 145 | - 项目内部的 vue-cli-service 命令,提供 serve、build 和 inspect 命令 146 | 147 | 要理解 CLI 的一些配置,我们先要来理解一下 Webpcak 的一些概念。本质上,Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 Webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。这里我们主要介绍搭建时涉及的一些配置。 148 | 149 | **四个核心概念:入口(entry)、输出(output)、loader、插件(plugins)。** 150 | 151 | #### 入口(entry) 152 | 153 | 入口(entry)将您应用程序的入口起点认为是根上下文(contextual root)或 app 第一个启动文件。这个概念的理解可以举例来说明下,例如在 Vue 中是`new Vue()`位置所在的文件,在 Angular 中是启动`.bootstrap()`的文件,在 React 中则是`ReactDOM.render()`或者是`React.render()`的启动文件。 154 | 155 | ```js 156 | // 将entry指向启动文件即可 157 | module.exports = { 158 | entry: "./path/to/my/entry/file.js" 159 | }; 160 | 161 | // 我们来看看,Vue CLI里源码是怎样的: 162 | webpackConfig 163 | .entry("app") 164 | .add("./src/main.js") 165 | .end(); 166 | ``` 167 | 168 | 显然,Vue CLI 的默认入口文件是./src/main.js。我们能看到 Vue CLI 内部的 webpack 配置是通过链式调用的,该能力通过 webpack-chain 库提供的。这个库提供了一个 webpack 原始配置的上层抽象,使其可以定义具名的 loader 规则和具名插件,并有机会在后期进入这些规则并对它们的选项进行修改。 169 | 170 | #### 出口(output) 171 | 172 | 出口(output)属性描述了如何处理归拢在一起的代码(bundled code),在哪里打包应用程序。简单来说,就是最终打包好的代码放哪。一般需要以下两点配置: 173 | (1) filename: 编译文件的文件名(main.js/bundle.js/index.js 等)。 174 | (2) path:对应一个绝对路径,此路径是你希望一次性打包的目录。 175 | 176 | ```js 177 | // 这是一般的Webpack写法 178 | module.exports = { 179 | output: { 180 | filename: "bundle.js", 181 | path: "/home/proj/public/assets" 182 | } 183 | }; 184 | 185 | // 我们来看看,Vue CLI源码的实现: 186 | webpackConfig.output 187 | .path(api.resolve(options.outputDir)) 188 | .filename(isLegacyBundle ? "[name]-legacy.js" : "[name].js") 189 | .publicPath(options.publicPath); 190 | ``` 191 | 192 | #### loader 193 | 194 | Webpack 把每个文件(.css, .html, .scss, .jpg, etc.) 都作为模块处理,但 Webpack 只理解 JavaScript。如果你看过生成的 bundle.js 代码就会发现,Webpack 将所有的模块打包一起,每个模块添加标记 id,通过这样一个 id 去获取所需模块的代码。而我们的 loader 的作用,就是把不同的模块和文件转换为这样一个模块,打包进去。 195 | 196 | loader 支持链式传递。能够对资源使用流水线(pipeline)。loader 链式地按照先后顺序进行编译,从后往前,最终需要返回 javascript。不同的应用场景需要不同的 loader,这里我简单介绍几个常用的(loader 使用前都需要安装,请自行查找依赖安装)。 197 | 198 | **babel-loader** 199 | babel-loader 将 ES6/ES7 语法编译生成 ES5,当然有些特性还是需要 babel-polyfill 支持的(Babel 默认只转换新的 JavaScript 句法,而不转换新的 API,如 Promise 等全局对象)。而对于 babel-loader 的配置,可以通过`options`进行,但一般更常使用.babelrc 文件进行(使用 Vue CLI 生成的项目目录中,可以使用 babel.config.js 文件来配置): 200 | 201 | **css 相关 loader** 202 | 203 | - css-loader: 处理 css 文件中的 url() 204 | - style-loader: 将 css 插入到页面的 style 标签 205 | - less-loader: less 转换为 css 206 | - postcss-loader(autoprefixer-loader): 自动添加兼容前缀(`-webkit-`、`-moz-`等) 207 | 208 | **其他 loader** 209 | 210 | - url-loader/file-loader: 修改文件名,放在输出目录下,并返其对应的 url 211 | - url-loader 在当文件大小小于限制值时,它可以返回一个 Data Url 212 | - html-loader/raw-loader: 把 Html 文件输出成字符串 213 | - html-loader 默认处理 html 中的``为`require("./image.png")`,需要在配置中指定 image 文件的加载器 214 | 215 | 说了这么多,我们来看看 Vue CLI 里自带了多少的 Loader: 216 | 217 | ```js 218 | // 最基本的,Vue文件解析 219 | webpackConfig.module 220 | .rule("vue") 221 | // 命中后缀为.vue的文件 222 | .test(/\.vue$/) 223 | // 使用缓存cache-loader 224 | // 在一些性能开销较大的loader之前添加此loader,以将结果缓存到磁盘里 225 | .use("cache-loader") 226 | .loader("cache-loader") 227 | .options(vueLoaderCacheConfig) 228 | .end() 229 | // 使用vue-loader 230 | .use("vue-loader") 231 | .loader("vue-loader") 232 | .options( 233 | Object.assign( 234 | { 235 | compilerOptions: { 236 | preserveWhitespace: false 237 | } 238 | }, 239 | vueLoaderCacheConfig 240 | ) 241 | ); 242 | 243 | // 图片文件解析 244 | webpackConfig.module 245 | .rule("images") 246 | // 命中后缀为.png/.jpeg/.jpg/.gif/.webp的文件 247 | .test(/\.(png|jpe?g|gif|webp)(\?.*)?$/) 248 | // 使用url-loader 249 | // 修改文件名,放在输出目录 img 下,并返其对应的url 250 | // 默认情况下,生成文件的文件名,是文件内容的 MD5 哈希值 251 | .use("url-loader") 252 | .loader("url-loader") 253 | .options(genUrlLoaderOptions("img")); 254 | 255 | // do not base64-inline SVGs. 256 | // https://github.com/facebookincubator/create-react-app/pull/1180 257 | // svg文件解析 258 | webpackConfig.module 259 | .rule("svg") 260 | // 命中后缀为.svg的文件 261 | .test(/\.(svg)(\?.*)?$/) 262 | // 使用file-loader,与url-loader相似 263 | // 修改文件名,放在输出目录 img 下,并返其对应的url 264 | // 默认情况下,生成文件的文件名,是文件内容的 MD5 哈希值 265 | .use("file-loader") 266 | .loader("file-loader") 267 | .options({ 268 | name: genAssetSubPath("img") 269 | }); 270 | 271 | // 媒体文件解析 272 | webpackConfig.module 273 | .rule("media") 274 | // 命中后缀为以下的文件 275 | .test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/) 276 | // 使用url-loader,此处输出目录为 media 277 | .use("url-loader") 278 | .loader("url-loader") 279 | .options(genUrlLoaderOptions("media")); 280 | 281 | // 字体文件解析 282 | webpackConfig.module 283 | .rule("fonts") 284 | .test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i) 285 | // 使用url-loader,此处输出目录为 fonts 286 | .use("url-loader") 287 | .loader("url-loader") 288 | .options(genUrlLoaderOptions("fonts")); 289 | ``` 290 | 291 | 除了上面这些之外,还有 CSS Loader 一大堆,包括 extract-css-loader、vue-style-loader、css-loader、cssnano、postcss-loader、sass-loader、less-loader 等,篇幅关系,这里就不再展示,我们也可以明白以下的 CLI 说明: 292 | (1) 所有编译后的 CSS 都会通过 css-loader 来解析其中的`url()`引用,并将这些引用作为模块请求来处理。也就是说我们可以根据本地的文件结构用相对路径来引用静态资源。 293 | (2) 内置的 webpack 被预配置了预处理器,支持 Sass/Less/Stylus 处理。 294 | (3) Vue CLI 内部使用了 PostCSS,默认开启了 autoprefixer。 295 | 296 | 差点忘了,还有个特别重要的嘉宾没登场:babel-loader。babel 相关的被单独放在了 cli-plugin-babel 模块里了,我们来看看: 297 | 298 | ```js 299 | // babel-loader使用了插件的模式 300 | // 插件地址可参考@vue/cli-plugin-babel 301 | webpackConfig.module 302 | .rule('js') 303 | // 命中后缀为.js/.jsx/.mjs/.mjsx的文件 304 | .test(/\.m?jsx?$/) 305 | .exclude 306 | // 排除以下文件 307 | .add(filepath => { 308 | // blablabla 309 | }) 310 | .end() 311 | // 使用缓存cache-loader 312 | // 在一些性能开销较大的loader之前添加此loader,以将结果缓存到磁盘里 313 | .use('cache-loader') 314 | .loader('cache-loader') 315 | // 这里还有一堆配置blablabla,感兴趣的可以自行去翻 316 | .end() 317 | // 使用babel-loader 318 | .use('babel-loader') 319 | .loader('babel-loader') 320 | 321 | // 我们能看到,Vue CLI默认的babel配置如下: 322 | { 323 | babel: { 324 | presets: ['@vue/app'] 325 | }, 326 | dependencies: { 327 | 'core-js': '^2.6.5' 328 | } 329 | } 330 | ``` 331 | 332 | 这里使用的 babel 插件是@vue/babel-preset-app,它通过@babel/preset-env 和 browserslist 配置来决定项目需要的 polyfill。默认情况下,它会把`useBuiltIns: 'usage'`传递给@babel/preset-env,这样它会根据源代码中出现的语言特性自动检测需要的 polyfill。这确保了最终包里 polyfill 数量的最小化。 333 | 334 | #### 插件(plugins) 335 | 336 | loader 仅在每个文件的基础上执行转换,**插件目的在于解决 loader 无法实现的其他事**。由于 plugin 可以携带参数/选项,需要在 wepback 配置中,向 plugins 属性传入`new`实例。这里也介绍几个常用的插件: 337 | 338 | **HtmlwebpackPlugin** 339 | 功能有下: 340 | 341 | - 为 html 文件中引入的外部资源如 script、link 动态添加每次 compile 后的 hash,防止引用缓存的外部文件问题 342 | - 可以生成创建 html 入口文件,比如单页面可以生成一个 html 文件入口 343 | - 但其实最常使用的,无非是把 index.html 页面插入(因为入口文件为 js 文件) 344 | 345 | ```js 346 | // HtmlwebpackPlugin使用方式示例 347 | new HtmlwebpackPlugin({ 348 | template: path.resolve(__dirname, "src/index.html"), 349 | inject: "body" 350 | }); 351 | ``` 352 | 353 | **CommonsChunkPlugin** 354 | 提取代码中的公共模块,然后将公共模块打包到一个独立的文件中,以便在其他的入口和模块中使用。 355 | 356 | **webpack.ProvidePlugin** 357 | 定义标识符,当遇到指定标识符的时候,自动加载模块。适合引入的全局变量,像我们常用的 jQuery: 358 | 359 | ```js 360 | // webpack.ProvidePlugin使用方式示例 361 | new webpack.ProvidePlugin({ 362 | jQuery: "jquery", 363 | $: "jquery" 364 | }); 365 | ``` 366 | 367 | **ExtractTextPlugin** 368 | 可以将样式从 js 中抽出,生成单独的.css 样式文件。即把所以的 css 打包合并: 369 | 370 | ```js 371 | // ExtractTextPlugin使用方式示例 372 | new ExtractTextPlugin("style.css", { 373 | allChunks: true // 提取所有的chunk(默认只提取initial chunk,而上面CommonsChunkPlugin已经把部分抽离了) 374 | }); 375 | ``` 376 | 377 | 我们看看 Vue CLI 中默认都使用了哪些插件: 378 | 379 | ```js 380 | // html-webpack-plugin插件 381 | const HTMLPlugin = require("html-webpack-plugin"); 382 | // 这是html-webpack-plugin的扩展插件 383 | const PreloadPlugin = require("@vue/preload-webpack-plugin"); 384 | 385 | webpackConfig.plugin("html").use(HTMLPlugin, [htmlOptions]); 386 | 387 | // 用于自动连接异步(和其他类型)的JavaScript块。这有助于延迟加载 388 | webpackConfig.plugin("preload").use(PreloadPlugin, [ 389 | { 390 | rel: "preload", 391 | include: "initial", 392 | fileBlacklist: [/\.map$/, /hot-update\.js$/] 393 | } 394 | ]); 395 | 396 | // 来告诉浏览器在页面加载完成后,利用空闲时间提前获取用户未来可能会访问的内容。 397 | webpackConfig.plugin("prefetch").use(PreloadPlugin, [ 398 | { 399 | rel: "prefetch", 400 | include: "asyncChunks" 401 | } 402 | ]); 403 | 404 | // 简单地复制public文件夹下的内容 405 | const publicDir = api.resolve("public"); 406 | webpackConfig.plugin("copy").use(require("copy-webpack-plugin"), [ 407 | [ 408 | { 409 | from: publicDir, 410 | to: outputDir, 411 | toType: "dir", 412 | ignore: publicCopyIgnore 413 | } 414 | ] 415 | ]); 416 | 417 | // 为什么不为html-webpack-plugin设置文件名? 418 | // 1.它无法处理绝对路径 419 | // 2.相对路径导致生成错误的SW清单(#2007) 420 | webpackConfig 421 | .plugin("move-index") 422 | .use(require("../webpack/MovePlugin"), [ 423 | path.resolve(outputDir, "index.html"), 424 | path.resolve(outputDir, options.indexPath) 425 | ]); 426 | 427 | // DefinePlugin允许创建一个在编译时可以配置的全局常量 428 | webpackConfig 429 | .plugin("define") 430 | .use(require("webpack/lib/DefinePlugin"), [resolveClientEnv(options)]); 431 | 432 | // 友好的错误插件,在webpack时显示非常混乱的错误 433 | // 无法解析加载器,因此我们提供自定义处理程序来改进它 434 | const { transformer, formatter } = require("../util/resolveLoaderError"); 435 | webpackConfig 436 | .plugin("friendly-errors") 437 | .use(require("@soda/friendly-errors-webpack-plugin"), [ 438 | { 439 | additionalTransformers: [transformer], 440 | additionalFormatters: [formatter] 441 | } 442 | ]); 443 | 444 | // 热加载的插件 445 | webpackConfig 446 | .plugin("hmr") 447 | .use(require("webpack/lib/HotModuleReplacementPlugin")); 448 | ``` 449 | 450 | 看到这些插件,我们可以很快地理解一些 CLI 的基本情况: 451 | (1) public/index.html 文件是一个会被 html-webpack-plugin 处理的模板。在构建过程中,资源链接会被自动注入。 452 | (2) ``用来指定页面加载后很快会被用到的资源,所以在页面加载的过程中,我们希望在浏览器开始主体渲染之前尽早 preload。默认情况下,一个 Vue CLI 应用会为所有初始化渲染需要的文件自动生成 preload 提示。 453 | (3) ``用来告诉浏览器在页面加载完成后,利用空闲时间提前获取用户未来可能会访问的内容。默认情况下,一个 Vue CLI 应用会为所有作为 async chunk 生成的 JavaScript 文件自动生成 prefetch 提示。 454 | (4) 任何放置在 public 文件夹的静态资源都会被简单的复制,而不经过 webpack。你需要通过绝对路径来引用它们。 455 | 456 | 上面只是介绍了一部分,大家也可以自行查看 Vue CLI 的源码来看。 457 | 458 | #### 解析(resolve) 459 | 460 | 这些选项能设置模块如何被解析,因为这里会使用到所以也介绍一下用到的: 461 | 462 | - resolve.extensions 463 | - 自动解析确定的扩展。默认值为:`[".js", ".json"]` 464 | - resolve.modules 465 | 模块将在 resolve.modules 中指定的所有目录内搜索。 466 | - resolve.alias 467 | - 创建`import`或`require`的别名,来确保模块引入变得更简单。如果使用 typescript 的话,我们还需要配置 tsconfig.json 468 | 469 | 我们来看看 Vue CLI 提供的默认配置: 470 | 471 | ```js 472 | webpackConfig.resolve.extensions // 此处为支持解析的文件名后缀 473 | .merge([".mjs", ".js", ".jsx", ".vue", ".json", ".wasm"]) 474 | .end() 475 | .modules // 这里所有的模块,我们都在 node_modules 目录下搜索 476 | .add("node_modules") 477 | .add(api.resolve("node_modules")) 478 | .add(resolveLocal("node_modules")) 479 | .end() 480 | .alias // 我们能看到,在Vue CLI生成的项目里,可以直接使用 @ 映射到 src 目录下 481 | .set("@", api.resolve("src")) 482 | .set( 483 | "vue$", 484 | options.runtimeCompiler 485 | ? "vue/dist/vue.esm.js" 486 | : "vue/dist/vue.runtime.esm.js" 487 | ); 488 | ``` 489 | 490 | #### 在 Vue CLI 里配置 Webpack 491 | 492 | 虽然 Vue CLI 里初始化了很多 Webpack 的配置,也内置了很多 Loader、Plugin,但如果我们需要自己进行一些 Webpack 配置的时候,CLI 也开放了接口给我们使用。调整 webpack 配置有几种种方式。 493 | 494 | (1) 最简单的方式就是在 vue.config.js 中的 configureWebpack 选项提供一个对象: 495 | 496 | ```js 497 | // vue.config.js 498 | module.exports = { 499 | configureWebpack: { 500 | plugins: [ 501 | // 没办法,我还是需要使用jQuery 502 | new webpack.ProvidePlugin({ 503 | jQuery: "jquery", 504 | $: "jquery" 505 | }) 506 | ] 507 | } 508 | }; 509 | ``` 510 | 511 | (2) 如果你需要基于环境有条件地配置行为,或者想要直接修改配置,那就换成一个函数 (该函数会在环境变量被设置之后懒执行)。该方法的第一个参数会收到已经解析好的配置。在函数内,你可以直接修改配置,或者返回一个将会被合并的对象: 512 | 513 | ```js 514 | // vue.config.js 515 | module.exports = { 516 | configureWebpack: config => { 517 | if (process.env.NODE_ENV === "production") { 518 | // 为生产环境修改配置... 519 | } else { 520 | // 为开发环境修改配置... 521 | } 522 | } 523 | }; 524 | ``` 525 | 526 | (3) 在 vue.config.js 中的 chainWebpack 修改,允许我们更细粒度的控制其内部配置。例如: 527 | 528 | ```js 529 | // vue.config.js 530 | module.exports = { 531 | filenameHashing: false, 532 | chainWebpack: config => { 533 | // 我不想要预加载的preload和prefetch 534 | // delete删除HTML相关的preload和prefetch webpack插件 535 | config.plugins.delete("preload"); 536 | config.plugins.delete("prefetch"); 537 | 538 | // 我想要使用typescript 539 | // 加个loader 540 | config 541 | .rule("ts") 542 | .test(/\.ts$/) 543 | .use("ts-loader"); 544 | } 545 | }; 546 | ``` 547 | 548 | ### 2.2.3 更多配置和使用方法 549 | 550 | 这里介绍一些 Vue CLI 的其他使用方法,以及一些常用的配置方式。 551 | 552 | #### 快速原型开发 553 | 554 | 前面提到,如果只是写一段简单的代码来跑 Demo,还得在本地生成个 HTML 文件然后在浏览器打开。当然,使用在线平台来写代码会更方便。那如果是 Vue 的代码呢?我们不光要生成个 Vue 文件,同时还得搭配一整套环境来测试,这个时候,我们就可以使用 Vue CLI 的快速原型开发了。例如还是这个代码: 555 | 556 | ```html 557 | 558 | 561 | 562 | 571 | ``` 572 | 573 | 使用快速原型开发,需要先额外安装一个全局的扩展(额外的意思是,请记得也全局安装脚手架`npm install -g @vue/cli`噢): 574 | 575 | ```cmd 576 | npm install -g @vue/cli-service-global 577 | ``` 578 | 579 | 然后在目录下可以使用`vue serve`和`vue build`命令对单个\*.vue 文件进行快速原型开发。这里我们在 App.vue 目录文件下运行`vue serve App.vue`: 580 | 581 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-2-3.jpg) 582 | 图 2-6 Vue CLI 启动快速原型开发 583 | 584 | 然后我们打开`http://localhost:8080/`或者`http://10.40.52.42:8080/`就能看到运行效果了: 585 | 586 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-2-4.jpg) 587 | 图 2-7 Vue CLI 启动快速原型开发效果 588 | 589 | 如果要直接部署使用,可以执行`vue build App.vue`将目标文件构建成一个生产环境的包,我们最终能得到这样的代码: 590 | 591 | ![image](https://github-imglib-1255459943.cos.ap-chengdu.myqcloud.com/vue-2-5.jpg) 592 | 图 2-8 Vue CLI 快速原型开发代码构建 593 | 594 | `vue serve`和`vue build`只适用于快速原型开发,如果需要,你还可以提供一个 index.html、package.json、安装并使用本地依赖、甚至通过相应的配置文件配置 Babel、PostCSS 和 ESLint。更多的使用大家可以去官网查看,这里就不多说了。 595 | 596 | #### 其他配置 597 | 598 | 除了前面讲到的一些 Webpack 配置,Loader、Plugins 相关的,我们还有一些可能会用到的其他配置。其实更多的可能是比较基础的配置: 599 | 600 | ```js 601 | module.exports = { 602 | publicPath: "/", // 部署应用包时的基本 URL 603 | outputDir: "dist", // 当运行 vue-cli-service build 时生成的生产环境构建文件的目录 604 | assetsDir: "", // 放置生成的静态资源 (js、css、img、fonts) 的 (相对于 outputDir 的) 目录 605 | indexPath: "index.html", // 指定生成的 index.html 的输出路径 (相对于 outputDir) 606 | filenameHashing: true, // 默认情况下,生成的静态资源在它们的文件名中包含了 hash 以便更好的控制缓存 607 | pages: undefined, // 在 multi-page 模式下构建应用 608 | lintOnSave: true, // 是否在开发环境下通过 eslint-loader 在每次保存时 lint 代码 609 | 610 | // 如果这个值是一个对象,则会通过 webpack-merge 合并到最终的配置中 611 | // 如果这个值是一个函数,则会接收被解析的配置作为参数。该函数及可以修改配置并不返回任何东西,也可以返回一个被克隆或合并过的配置版本 612 | configureWebpack: undefined, 613 | 614 | // 是一个函数 615 | // 会接收一个基于 webpack-chain 的 ChainableConfig 实例。允许对内部的 webpack 配置进行更细粒度的修改 616 | chainWebpack: undefined, 617 | 618 | devServer: undefined // 所有 webpack-dev-server 的选项都支持 619 | }; 620 | ``` 621 | 622 | 目前比较常用的大概就是上面这些了,关于多页应用的会在[《第14章 实战:使用 Webpack 或 Vue CLI 搭建多页应用》](./14.md)介绍,更多的大家可以自行探索使用方式。 623 | 624 | 在很久以前,Vue CLI 还会把内置的 webpack 配置暴露在开发者的项目中,现在已经通过内置的方式来提供很多的通用和基础能力,开发者可以直接使用而无需进行很多的配置。与此同时,Vue CLI 还提供了很多的配置给到开发者进行灵活配置,真的特别棒。 625 | -------------------------------------------------------------------------------- /docs/vue-ebook/9.md: -------------------------------------------------------------------------------- 1 | # 第9章 思维转变与大型项目管理 2 | 3 | 这些年来前端发展进步了很多,不管是工具、框架的丰富,还是前端在各个领域的应用拓展,越来越多的人开始加入前端大军。而随着拥有的能力越来越多,如果我们不在适当的时候进行梳理和调整,越到后面就越抽不出时间进行思考,因为已经被死亡代码(俗称屎山)埋没了。为了避免这种事情的发生,我也常常进行一些思考和自我调整,一起分享给你们。 4 | 5 | ## 9.1 编码思维转变 6 | 7 | 首先介绍事件驱动和数据驱动两种编码思维模式,其实两种写码方式它和你使用 jQuery 还是使用 Vue 等框架并没有多大的关系,更多会在于设计代码时的一个思考方式。 8 | 9 | 但事实上当年我在从 jQuery 切换到 AngularJS 的时候,也是常常满屏的疑惑,不知道从何下手。而事件驱动和数据驱动的思维调整,也是在这一个过渡过程体会比较深刻,所以这里还是会结合 jQuery 和 Vue 来讲解一下这样的转变。 10 | 11 | 我们先来看看事件驱动的编码方式。 12 | 13 | ### 9.1.1 事件驱动 14 | 15 | 由于前端是页面交互出身的,运作模式也是基于 I/O 模式。你知道为什么 JavaScript 是单线程的吗?其实更多是因为对页面交互的同步处理。作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM,若是多线程会导致严重的同步问题。 16 | 17 | 为什么这么说呢?我们可以从 GUI(图形用户界面)来讲起。 18 | 19 | #### GUI 与事件 20 | 21 | GUI(图形用户界面)与事件驱动的渊源可谓不浅。GUI 应用程序的特点是注重与用户的交互,因此程序的执行取决于与用户的实时交互情况,大部分的程序执行需要等到用户的交互动作发生之后。由于用户的输入频率并不高,若不停轮询获取用户输入(点像 ajax 轮询),这样的方式存在以下问题: 22 | (1) 资源利用率低。 23 | (2) 不能真正做到及时同步。 24 | 25 | 由于 GUI 程序的执行流程由用户控制,并且不可预期,为了适应这种特点,我们通常采用事件驱动的编程方法。程序对事件的响应,其实就是调用预先编制好的代码来对事件进行处理。 26 | 27 | 如果 Javascript 完全使用同步的单线程方式来执行,我们就无法对多个事件进行监听。除此之外,我们的页面交互就会变得很慢,还会有很大一部分的等待时间,造成很多资源浪费。所以 Javascript 是异步的,支持多个事件的并发,而 JavaScript 的并发模型基于“事件循环”。在 Javascript 中,主线程从"任务队列"中读取事件,这个过程是循环不断的,整个的这种运行机制又称为 Event Loop(事件循环)。 28 | 29 | 在 GUI 的使用场景和并发的 Javascript 设计下,我们写代码的时候也会代入这样的思维:**用户输入 => 事件响应 => 代码运行 => 刷新页面状态**。于是乎,刚开始写页面功能的思路如下: 30 | (1) 开发静态页面。 31 | (2) 添加事件监听,包括用户输入、http 请求、定时器触发等事件。 32 | (3) 针对不同事件,编写不同的处理逻辑,包括获取事件状态/输入、计算并更新状态等。 33 | (4) 根据计算后的数据状态,重新渲染页面。 34 | 35 | 通俗地说,事件驱动思维是从事件响应出发,来完成应用的设计和编程。事件驱动的编码方式,在 jQuery 提供了链式调用、方便的`$()`元素选择、`on()`事件监听之后进入了热潮,我们经常会写这样的代码: 36 | 37 | ```js 38 | // 某个父元素上监听多个子节点的事件并处理 39 | // setLength 函数用来限制长度 40 | someDom 41 | // 子节点 A 是输入框,样式为 classA 42 | .on("keyup", ".classA", function(ev) { 43 | inputObject.a = setLength(ev, 16); 44 | }) 45 | // 子节点 B 也是输入框,样式为 classB 46 | .on("keyup", ".classB", function(ev) { 47 | inputObject.b = setLength(ev, 8); 48 | }) 49 | // 子节点 C 也是输入框,样式为 classC 50 | .on("keyup", ".classC", function(ev) { 51 | inputObject.c = setLength(ev, 8); 52 | }) 53 | // 子节点 D 为下拉列表,样式为 classD 54 | .on("change", ".classD", function(ev) { 55 | inputObject.d = ev.target.value; 56 | }); 57 | ``` 58 | 59 | 在 jQuery 的帮助下,事件绑定写起来真的太爽啦,虽然可读性和维护性上都有缺陷,但在当时也是性价比很高的开发方式了。所以当时很多开发者会选择(或是不自主地)使用事件驱动方式来写代码。 60 | 61 | #### 事件驱动的流程 62 | 63 | 事件驱动其实是前端开发中最容易理解的编码方式,例如我们写一个提交表单的页面,用事件驱动的方式来写的话,会是这样一个流程: 64 | 65 | **(1) 编写静态页面。** 66 | 67 | ```html 68 | 69 |
70 | Name: 71 |

72 | 73 | Email: 74 |

75 | 76 | 77 |
78 | ``` 79 | 80 | **(2) 给对应的元素绑定对应的事件。** 81 | 82 | 例如给 input 输入框绑定输入事件,在前端页面中绑定事件监听通过`addEventListener`来实现: 83 | 84 | ```js 85 | var nameInputEl = document.getElementById("name-input"); 86 | var emailInputEl = document.getElementById("email-input"); 87 | // 监听输入事件,此时 updateValue 函数未定义 88 | nameInputEl.addEventListener("input", updateNameValue); 89 | emailInputEl.addEventListener("input", updateEmailValue); 90 | ``` 91 | 92 | **(3) 事件触发时,更新页面内容。** 93 | 94 | 我们给元素绑定了事件监听之后,在事件触发的时候,我们需要进行相关逻辑的处理(发起请求、更新页面内容等),这里我们将用户输入的内容更新到页面中展示: 95 | 96 | ```js 97 | var nameValueEl = document.getElementById("name-value"); 98 | var emailValueEl = document.getElementById("email-value"); 99 | // 定义 updateValue 函数,用来更新页面内容 100 | function updateNameValue(e) { 101 | nameValueEl.innerText = e.srcElement.value; 102 | } 103 | function updateEmailValue(e) { 104 | emailValueEl.innerText = e.srcElement.value; 105 | } 106 | ``` 107 | 108 | 以上这个流程,是很常见的前端编码思维,我们称之为**事件驱动**模式。如果使用了 Vue,也是可以很容易用事件驱动的方式这样写代码: 109 | 110 | ```html 111 | 122 | 123 | 138 | ``` 139 | 140 | Vue 帮我们省去了元素选择、HTML 拼接并更新等这些工作,同时直接在模板上绑定的方式也简化了(使用`v-on:`或者`@`),方便开发者阅读和理解。 141 | 142 | 我们再来回顾下事件驱动的方式: 143 | (1) 开发静态页面。 144 | (2) 在对应的元素上绑定事件。 145 | (3) 实现被绑定的事件功能,例如获取数据、更新页面等。 146 | 147 | 整个思考的链路在于**触发了怎样的操作**和**这个操作会导致什么后果(即需要做怎样的处理)**,事件驱动的思维方式都是围绕“操作”(在前端语言中,也就是“事件”),我们跟随着“操作”的链路来实现代码编写。 148 | 149 | 如今前端页面交互也越来越复杂,我们在设计功能的时候,也常常需要使用抽象的能力。作为程序员,我们最常用的抽象方式就是数据抽象,而前端的界面、组件、配置等,都可以抽象成数据表达。关于怎么进行抽象,[《第10章 如何正确地进行抽象》](./10.md)内容会详细介绍。 150 | 151 | 我们看看数据驱动的思维方式是怎样的。 152 | 153 | ### 9.1.2 数据驱动 154 | 155 | 其实不管是生活中还是工作中,几乎所有的事物我们都可以抽象为数据。像游戏里面的角色、物品、经验值、天气、时间等等,都是数据。游戏其实也算是对真实世界抽象的一种,而抽象之后,最终都可呈现为数据。 156 | 157 | 如果要对事件驱动和数据驱动进行直观的比较,其实最大的转变是,以前会把组件视为 DOM,把事件/逻辑处理视为 Javascript,把样式视为 CSS。而当转换思维方式之后,组件、事件、逻辑处理、样式都是一份数据,我们只需要把数据的状态和转换设计好,剩下的实现则由具现方式(模版引擎、事件机制等)来实现。 158 | 159 | #### 数据驱动的流程 160 | 161 | 既然前面介绍了事件模型一般的编码流程,我们再来看看,同样的写一个提交表单的页面,用数据驱动的方式来写的话,会是下面这样的步骤过程。 162 | 163 | **(1) 设计数据结构。** 164 | 165 | 首先我们需要,将页面中会变化和不会变化的内容隔离开,然后对其中会变化的内容进行抽象,再根据抽象结果来设计数据结构。例如这里的表单,可变的部分包括两个输入框、两处展示输入框内容的文字。但其实涉的数据只有两个,一个是名字`name`,另外一个是邮件`email`,都可以用字符串表示: 166 | 167 | ```js 168 | // 包括一个 name 和 一个 email 的值 169 | export default { 170 | data() { 171 | return { 172 | name: "", 173 | email: "" 174 | }; 175 | } 176 | }; 177 | ``` 178 | 179 | **(2) 完成静态页面,同时把数据和事件绑定到页面中。** 180 | 181 | 接下来我们把静态页面开发出来,然后将步骤(1)中的数据绑定到页面中需要使用/展示的地方,同时在一些事件触发的元素上绑定对应的方法: 182 | 183 | ```html 184 |
185 | Name: 186 |

{{ name }}

187 | 193 | Email: 194 |

{{ email }}

195 | 201 | 202 |
203 | ``` 204 | 205 | **(3) 事件绑定的方法(methods)中,补充相应的逻辑处理。** 206 | 207 | 我们在第(2)步中绑定了一下事件监听,主要是两个输入框`v-on:input`绑定的输入事件,我们需要在用户输入的同时更新到`data`中: 208 | 209 | ```js 210 | export default { 211 | data() { 212 | return { 213 | name: "", 214 | email: "" 215 | }; 216 | }, 217 | methods: { 218 | // 绑定 input 事件,获取到输入值,设置到对应的数据中 219 | updateNameValue(event) { 220 | this.name = event.target.value; 221 | }, 222 | updateEmailValue(event) { 223 | this.email = event.target.value; 224 | } 225 | } 226 | }; 227 | ``` 228 | 229 | 我们在设置数据(`this.name = event.target.value`)的时候,Vue 会自动帮我们更新页面中绑定该数据的内容(`{{ name }}`和`{{ email }}`处),我们就不用自己手动获取元素然后更新节点内容了。 230 | 231 | 其实我们也可以先开发静态模板,然后根据可变的内容来设计数据结构。事件驱动和数据驱动一个很重要的区别在于,我们是从每个事件的触发(“操作”)为中心来设计我们的代码,还是**以数据为中心**,接收事件触发和更新数据状态的方式来写代码。 232 | 233 | 我们再来详细地对比一下。 234 | 235 | ### 9.1.3 数据驱动和事件驱动 236 | 237 | 这里或许你们会有些疑问,看起来只是写代码的顺序不一样而已,甚至写代码的顺序都是一样的,那事件驱动和数据驱动的区别在哪?一个很有用的区别在于,从事件驱动转换到数据驱动思维后,我们在编程实现的过程中,更多的是思考数据状态的维护和处理,而无需过于考虑 UI 的变化和事件的监听,即使我们页面全部重构了,影响到的只有模板中绑定的部分,重新绑定一下就可以了。 238 | 239 | 使用数据驱动来写代码会强迫开发者有一个前置条件,你会需要去设计一个数据结构,或者也可以称之为一个模型。在代码开发前的数据设计会有什么好处呢? 240 | 241 | #### 数据的获取和修改 242 | 243 | 我们在设计数据的时候,会进行将页面抽象成数据的一个步骤,例如一个表单里的内容可以抽象成一个对象,而一个列表中的内容可以表达成一个由对象组成的数组。 244 | 245 | 在 Vue 中,我们可以直接将数据绑定到页面元素中,而当这些内容变动的时候,我们只需要按照设计的数据格式来更新数据就可以。例如,我们新建了一个表单,然后在从后台拉取数据、获取填写的内容进行校验并提交到后台、提交成功后清空已填写内容等,可以这么实现: 246 | 247 | ```html 248 | 256 | 305 | ``` 306 | 307 | 在这整个过程中(获取数据并更新到页面、获取用户输入的内容、清空输入框内容),我们只需要获取和修改`phoneInfo`这个数据就可以了。而 Vue 框架会帮我们完成从页面元素获取数据,以及将数据更新到页面展示中这些工作。那是否意味着使用 jQuery 就不可以这样做了呢?并不是,如果要在 jQuery 中使用数据驱动,我们需要自己去实现从页面获取数据、更新到页面中这样的逻辑,例如: 308 | 309 | ```js 310 | function getPhoneInfo() { 311 | // 获取数据并返回 312 | const name = $("#name").val(); 313 | const phone = $("#phone").val(); 314 | const address = $("#address").val(); 315 | return { 316 | name, 317 | phone, 318 | address 319 | }; 320 | } 321 | function setPhoneInfo(phoneInfo) { 322 | const { name, phone, address } = phoneInfo; 323 | // 给元素设置数据 324 | $("#name").val(name); 325 | $("#phone").val(phone); 326 | $("#address").val(address); 327 | } 328 | ``` 329 | 330 | 这样,我们在 jQuery 中实现以上逻辑应该是这样: 331 | 332 | ```html 333 |
334 | 335 | 336 | 337 | 338 |
339 | 373 | ``` 374 | 375 | 所以,其实使用数据驱动还是事件驱动,跟使用 jQuery 还是 Vue 并没有多大关系,只是我们在整个页面的交互过程,从以往的从用户交互为中心,调整成以数据的状态扭转为中心,来进行一些逻辑的实现。 376 | 377 | 但是数据驱动是否又跟 Vue 完全没关系呢?对我个人来说,Vue、Angular、React 这些前端框架的出现,推动了我从事件驱动转变成数据驱动,从而我能更好地使用这些框架。技术的迭代、工具的更新和个人的成长,有时候是相辅相成的。 378 | 379 | #### 事件流与数据流 380 | 381 | 使用数据驱动还有一个好处,就是基于模型设计的代码,即使经历了需求变更、页面结构调整、后台接口调整,也可以快速地实现更新和支持。还是以上面的表单作为例子,我们在基于事件驱动开发,通常的思考和写代码的方式是: 382 | 383 | - 页面加载时 -> 请求后台 -> 拿到数据 -> 更新到页面 384 | - 用户点击提交时 -> 获取用户输入内容 -> (校验用户输入内容 ->) 提交给后台 -> 清空已输入内容 385 | 386 | 也就是说,事件驱动的特点是,以某个交互操作为起点,流程式地处理逻辑。流程式的代码,在遇到中间某个环节变更,就需要同时更新该变更点前后环节的流程交接。例如我们在页面加载的时候,需要先加载本地缓存,再从后台请求更新。如果是上面的流程,我们需要新增“本地获取缓存”的环节,同时需要在“页面加载时”、“更新到页面”两个环节进行衔接。 387 | 388 | 而数据驱动的思考方式特点是,以数据为中心,思考数据的输入和输出: 389 | 390 | - `phoneInfo`的数据来源包括两个:从后台获取、用户输入、重置清空 391 | - `phoneInfo`的数据去处包括:提交给后台 392 | 393 | 同样的,如果我们需新增“本地获取缓存”的环节,在数据驱动的情况下,只是增加了一个数据来源,对于整个模型影响会小很多: 394 | 395 | - `phoneInfo`的数据来源包括两个:`从本地缓存获取`、从后台获取、用户输入、重置清空 396 | 397 | 其实在我们日常开发中,更多时候是结合了事件驱动和数据驱动来使用。而如果现在的你刚入门没多久,也不用纠结是否真的用了某种思维模式、是否用了什么设计模式,我们在一次次的开发过程中,会不断地积累和加深一些思考,适合自己的才是最好的。 398 | 399 | ## 9.2 大型应用管理 400 | 401 | 说到大型应用,常见的我们项目中需要考虑加载性能和加载速度相关的,这些在很多前端相关的文章或者书籍中都可找到。这里主要介绍一些在大型项目中会使用到的工具,以及好用的技巧,还有项目规范、合作开发等经验,供大家参考。 402 | 403 | ### 9.2.1 代码打包 404 | 405 | 我们先从最基础的代码打包来讲起。 406 | 407 | #### 路由懒加载 408 | 409 | 当我们的应用变得很大,为了提升首屏加载的体验,我们需要对代码进行分块打包。一般来说,不同的框架有不同的异步加载解决方案,同时可以结合打包工具(Webpack、Gulp 等)进行分块打包。我们可以把首屏相关的东西打包到 bundle 文件中,其他模块分块打包到 chunk 文件,首页只需要加载 bundle 文件,然后在空闲的时候或者需要使用的时候再按需加载 chunk 文件模块。 410 | 411 | 通常情况下,我们会结合路由进行分块打包,路由管理工具大部分都支持异步加载。我们可以根据自己需要,来打包成多个文件,在路由进入的时候才获取和加载。Vue 可参考[《第7章 Vue Router 路由搭建应用》](./7.md)中路由懒加载相关内容。 412 | 413 | #### Source Map 414 | 415 | 这里需要讲一下,Source Map 就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。有了它,我们在定位压缩后的代码时,浏览器可以将直接显示原始代码,而不是压缩后的代码。这无疑给开发者带来了很大的便利。在开发环境下,还能通过 Chrome 匹配源文件进行在线 debug 和修复源码。大家也可以自行搜索下,进行了适当的配置之后,我们可以在浏览器上直接调试 CSS 并保存到本地文件,体验真的很棒。 416 | 417 | #### Tree-shaking 418 | 419 | 我们在引入一些开源代码或是公共库的时候,其实大部分时间我们都只是使用其中里面的一小部分代码。Tree-shaking 支持按需打包,没有被引用的模块不会被打包进来,减少我们的包大小,缩小应用的加载时间,给用户更好的体验。 420 | 421 | Tree-shaking 最初是[Rollup](http://www.rollupjs.com/)提出并实现。Rollup 会静态分析代码中的 import,并将排除任何未实际使用的代码。这允许我们架构于现有工具和模块之上,而不会增加额外的依赖或使项目的大小膨胀。在 Webpack 2 里也加入了 Tree-shaking 新特性,而 Vue 3.0 中也对支持 Tree-shaking 进行了优化,降低了代码包大小,可以参考[《第16章 关于 Vue 3.0》](./16.md)。 422 | 423 | ### 9.2.2 抽象、组件化和配置化 424 | 425 | 在我们开始写重复的代码、或是进行较多的复制粘贴的时候,大概是时候需要考虑对组件进行适当的抽象(也可以成为封装)了。好的抽象能大量减少重复代码,同时对项目整体有更深入的理解。过度的抽象会增加项目的复杂度,同时降低了代码的可读性和维护性。所以关键在于适度,好的办法是结合产品和业务来进行抽象,例如一个播放器组件、日历组件、快速导航栏、快捷菜单等组件封装,便于多次使用。 426 | 427 | 应用的抽象、配置化相关,可以参考[《第10章 如何正确地进行抽象》](./10.md)和[《第13章 实战:表单配置化实现》](./13.md)。同时,我们也需要把一些相同的方法抽离,封装成通用的工具库,像常用的时间转换、字符串处理、http 请求等,都可以单独拎出来维护。 428 | 429 | ### 9.2.3 状态和数据管理 430 | 431 | 我们的应用里,多数会面临组件的某些状态和数据相互影响、相互依赖的问题。现在也有比较成熟的解决方案和状态管理工具,像 Vuex、Redux、Mobx 等,我们需要结合自身的框架和业务场景来使用。像父子组件的交互、应用内无直接管理的数据状态共享、事件的传递等,也都需要结合实际适当地使用,可参考[《第11章 全局数据管理与 Vuex》](./11.md)。 432 | 433 | ### 9.2.4 代码流程规范 434 | 435 | 代码规范其实是团队合作中最重要的地方,使用统一的代码规范,会大大减少我们接手别人代码时候感觉到头疼的次数。好的代码习惯很重要,命名、适当的注释、适度的抽象等,会对代码的可读性有很大的提升。但是问题是每个人习惯都不一样,所以在此之上,我们需要有统一的代码规范。由于每个人习惯不一致,所以不可能让所有人都满意,代码规范的存在本身就已经有很重要的作用了。 436 | 437 | #### 项目结构 438 | 439 | 项目结构其实也很重要,我们在设计一个项目的时候,项目结构设计得清晰,维护就会越方便。项目结构设计有几个技巧: 440 | 441 | - 公共库、公共组件、公共配置分开维护 442 | - 静态资源文件单独放 443 | - 与构建相关的配置文件,可以放在最外层 444 | - 最后打包生成的文件,可以放在 dist 或者 built 目录下 445 | - README.md 文件放在最外层 446 | 447 | 在 Vue 中,我们可以这么组织: 448 | 449 | ``` 450 | ├─dist // 编译之后的项目文件 451 | ├─src // 开发目录 452 | │ ├─assets // 静态资源 453 | │ ├─css // 公共css 454 | │ ├─img // 图片资源 455 | │ ├─utils // 公共工具库 456 | │ ├─config // 公共配置 457 | │ ├─components // 公共组件 458 | │ ├─pages // 页面,根据路由结构划分 459 | │ ├─App.vue // 启动页面,最外层容器组件 460 | │ ├─main.js // 入口脚本 461 | │ 462 | ├─ babel.config.js // babel 配置文件 463 | ├─ vue.config.js // vue 自定义配置,与 webpack 配置相关 464 | ├─ package.json // 项目配置 465 | ├─ README.md // 项目说明 466 | ``` 467 | 468 | 同时,这样的结构可以写在 README 文件中维护,在协作过程、新人加入的时候就可以直接清晰地理解项目代码,也可以快速地找到需要的文件放置在哪里。而 README 在项目管理中也是很重要的一个文档,我们也来看看。 469 | 470 | #### 养成写 README 的习惯 471 | 472 | 一般来说,程序员拿到一个项目,首先要找一下文档。而程序员的文档基本上都是用 README 来管理的,一般来说 README 里面会包括: 473 | 474 | - 项目简单说明(背景、相关接口人) 475 | - 如何运行代码(安装、启动、构建) 476 | - 目录结构说明 477 | - 更多其他相关说明(配置文件、涉及文档) 478 | 479 | 如果涉及文档太多,也可以统一放置在 docs 文件夹下面。这样,新人在接手到这个项目的时候,可以根据 README 中的指引自行进行代码下载和运行,也可以快速地找到相关的指引和责任人。 480 | 481 | #### 代码流程规范工具 482 | 483 | 一些工具可以很好地协助我们,像 Eslint、Tslint 等(目前 Tslint 已不再维护,可以使用 Eslint),加上代码的打包工具协助,可以在一些流程中强校验代码规范。还可以使用像 prettier 这样的工具,能自动在打包的时候帮我们进行代码规范化。 484 | 485 | 除了这些简单的什么驼峰、全等、单引双引等基础的规范,其实更重要的是**流程规范**。最基础的比如改动公共库或是公共组件的时候,需要进行 code review。通常我们使用 Git 维护代码,通过 merge request 等方式来进行 code review,这样在合并或是版本控制上有更好的体验。但其实最重要的还是沟通,沟通是一个团队里必不可少、同时又很容易出问题的地方。 486 | 487 | 项目的维护永远是程序员的大头,一般来说前人种树后人乘凉。但是很多时候,大家会为了一时的方便,对代码规范比较随意,就导致了我们经常看到有人讨论“继承”来的代码,就变成了了前人挖坑后人填坑的方式了。或许相比新技术的研究和造轮子,有个好的写码习惯、提高项目维护性并不能带来短期的利益,但是其实身为一个负责任的程序员,还是要对有些追求的。 488 | 489 | 项目管理和维护其实是很重要的一件事,但这些在日常工作中常常会被忽视。更多人的关注点在系统上线,而运营和维护其实也是同样有价值和有作业的工作,我们也应该重视起来。 490 | 491 | 互联网在不断地发展,新技术层出不穷,处在叶子尖端的前端领域更是在不断摇晃。我们在一个陌生环境中,会习惯性用最熟悉的行为来继续。但如果以开放的心态来接受新事物,你会有想象不到的收获。 492 | -------------------------------------------------------------------------------- /docs/vue-ebook/99.md: -------------------------------------------------------------------------------- 1 | # 关于框架选型 2 | 3 | ## 几大框架的利弊 4 | 5 | 通常来说,框架选择是准备项目的第一步。说到框架,目前主流三大框架 Angular、React 和 Vue,先从个人理解来看看这三个框架。 6 | 7 | ### Angular 8 | 9 | > 这里的 Angular 是指 Angular 2.0+版本,v1.0 我们通常称之为 AngularJS,目前已经不更新了,如果要使用建议大家还是使用 Angular。 10 | 11 | Angular 相对 React 和 Vue,最初的设计是针对大型应用来进行的。要是你认识 JAVA 的话,像依赖注入这一套你会觉得很熟悉。当然到了 v2.0 以上的版本由于加入了很多的语法糖,看起来 AngularJS 和 Angular 相差很远,但是最核心的依赖注入模式还是相似的。 12 | 13 | 项目中使用 Angular,最大的体验感受则是项目有完备的结构和规范,新加入的成员能很快地通过复制粘贴完成功能的开发。有人说过,好的架构设计,能让高级程序员和初入门的程序员写出相似的代码,这样对于整体管理和项目的维护有非常好的体验。很多人说 Angular 难上手,其实主要在于开始的项目搭建、以及 Angular 独有的一套设计方案的理解。但是依赖注入的设计方式,我们几乎不用考虑很多数据和状态管理的问题。当然脏检查的方式曾经也带来性能问题,后面在加入树状的模块化、Zone.js 之后,即使没有虚拟 DOM,性能也是有大大的提升。 14 | 15 | 当初 AngularJS 拥有不少量的粉丝,但是在 AngularJS 到 Angular2 的断崖式升级下,不少的开发者开始弃用而投向 React、Vue 等框架。不得不说,Angular 这次升级是相当的激进,刚开始的时候连兼容方案都没有,虽然说核心的设计理念像依赖注入这种并没有很大的调整,但从 AngularJS 到 Angular2 几乎改变了所有的 API 和管理模式,同时引入了在当时还算前卫的 Typescript 以及各种语法糖,其实这样大胆的设计和自我革新,个人还是很佩服的。要往前走,有些东西最终还是要摒弃的。 16 | 17 | 从 Angular 框架设计大而全的角度来看的话,Vue 和 React 更多是小巧的模板框架,也可以通过灵活搭配路由、状态管理等工具,达到大型应用的管理。目前来说,社区也有比较好的解决方案,官方也有出相关的脚手架来快速构建应用。 18 | 19 | ### React 20 | 21 | 本人接触的 React 项目不是很多,但是 jsx、虚拟 DOM、函数式编程的设计,带来的震撼和冲击还是很大的。React 相对 Angular 最大的优势是轻量和性能,或许其实这么比较不大对,因为 React/Vue 和 Angular 不一样,Angular 是整套的解决方案,而 React/Vue 则是项目搭建中灵魂使用的前端模板工具。 22 | 23 | ```cmd 24 | # 也就是说,我们可以这么理解 25 | Angular = React + react-router + Redux/Flux/Mobx = Vue + vue-router + Vuex/Redux 26 | ``` 27 | 28 | 虚拟 DOM 主要解决了什么,它的原理又是怎样的,这些会涉及到浏览器的页面渲染原理,包括 DOM Tree、CSS Ruler Tree、Rendering Tree、Repaint、Reflow 等等,我们也在[《前言 前端框架的出现》](./0.md)和[《第 1 章 Vue 框架介绍》](./1.md)介绍过,你需要去理解虚拟 DOM 为何能带来性能的提升,这常常会被作为面试题来问噢。 29 | 30 | 以上,你会在使用 React 的时候,接触到很多好的设计,去引领你进行更深入的思考。函数式编程的方式,也会不同程度地拓展你的思考方式,遇到问题的时候,能有更多的解决办法。至于社区建设,其实三大主流开源框架的社区都相当完善了。 31 | 32 | ### Vue 33 | 34 | 如果你熟悉 Angular 以及 React,你会发现 Vue 的设计和使用,其实很多地方看起来很像像是前两者的结合体。Vue 结合了两者的优势,同时通过超友好的入门体验打下了一片天地。 35 | 36 | Vue 最大的特点是上手简单,不管是框架的设计和文档,都对新手极其友好。但是这并不代表它只是个简单的框架,当你需要实现一些更加深入的自定义功能时,你会发现它其实也有很好的功能支持。如果你还认为它只是把 Angular 和 React 的优势结合,在你深入使用甚至阅读源码的时候,你会慢慢发现里面处处都不缺 Vue 自己的思考,真的是个很棒的框架。 37 | 38 | 如果说你是个新手,那么 Vue 会是较好的选择。相比另外两个框架,Vue 最初的社区缺陷现在也早已不再是问题了,相关的脚手架、配套工具也都很完善,还有贴心的中文服务。 39 | 40 | #### 对比 Angular 41 | 42 | (1) 更灵活的适应各种场景。Vue 的默认 API 适合纯前端背景的开发者/小快灵场景,配合 TypeScript 也可以适合传统 Java 后端背景的开发者/大型项目。 43 | (2) 更低的培训成本,更快的上手速度。 44 | (3) 底层的 Virtual DOM 在高级场景下提供更多的灵活性。 45 | (4) 大型应用中与 TypeScript 的整合不如 Angular(Vue3.0 中会对 Typescript 支持更加友好)。 46 | 47 | #### 对比 React 48 | 49 | (1) 对大部分常见场景都提供了事实标准方案。不需要额外自行调研选取方案,在必要情况下也可以换用自研方案。 50 | (2) 模版提供更友好的学习曲线,同时暴露底层 Virtual DOM 用于高级场景(也支持 JSX)。 51 | (3) 大型应用中与 TypeScript 的整合暂时不如 React(同样的 Vue3.0 中会增强)。 52 | (4) 对标 React 16+,Vue 同样可以实现类似 Hooks 的逻辑复用机制(Vue3.0 支持时间分片)。 53 | 54 | ### 开源框架? 55 | 56 | 使用开源框架的好处是,有着完整详细的文档,同时有问题也能通过 issues 和 Stack Overflow 来查找。更多时候,我们选择一个框架,需要考虑项目大小、定位。**技术选型更多的在于团队,你要考虑这个团队的能力、大家对各个框架的熟悉程度、是否有强烈的倾向。**或者有能力的团队,也可以选择相对小众的框架。 57 | 58 | 当然也有些小伙伴喜欢自己造轮子,不过你们要记得,造轮子是要负责任的,你需要提供友好的文档和 API 给他人,不然对项目的维护来说,简直就是毁灭性的体验。有些热爱技术的小伙伴喜欢捣鼓新东西,个人的体验(填坑体验)是,我们尽量在对这些新技术有较好地把握之后,再尝试一点点加入我们的项目里。项目尤其是工程项目,大多数是解决某些问题,我们需要在满足业务和项目维护性的同时,来做一些新的尝试。 59 | 60 | ### 技术选型? 61 | 62 | 其实前端发展到现在,几大框架之间的差距越来越小,好的方案相互学习、不好的地方自行调整。从实现的功能上来看,Angular、React、Vue 并没有太大的区别,更多的是框架在设计的时候引入的一些概念带来的入门难度。 63 | 64 | 所以,技术选型在很多时候更多的是对业务和团队成员的考虑,包括 Typescript 等的使用,都是对配合的协助。大家一致性通过或者协商后的方案,才是最适合的方案。到后面更多的是维护成本,这个时候需要做些整理和抽象,这时候规范的重要性就随着项目的壮大、成员的增加愈发地体现出来了。技术选型,跟团队成员是无法脱离的。 65 | -------------------------------------------------------------------------------- /docs/vue-ebook/README.md: -------------------------------------------------------------------------------- 1 | 本书全面介绍 Vue.js 框架的出现、设计和使用,结合实战让读者更深入理解 Vue.js 框架,掌握使用方法。相关代码存放在[vue-sourcecode 分支](https://github.com/godbasin/vue-ebook/tree/vue-sourcecode)。 2 | - [Vue.js 官方文档](https://cn.vuejs.org/v2/guide/) 3 | 4 | [开始学习 ->](./vue-ebook/0.md) 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "vuepress dev docs", 4 | "build": "vuepress build docs" 5 | }, 6 | "dependencies": { 7 | "element-ui": "^2.10.1" 8 | }, 9 | "devDependencies": { 10 | "vuepress": "^1.8.2" 11 | } 12 | } 13 | --------------------------------------------------------------------------------