├── constants └── index.js ├── .husky ├── .gitignore ├── pre-commit ├── common.sh └── lintstagedrc.js ├── static ├── images │ ├── .gitkeep │ └── post-images │ │ ├── 也来扯扯-Vue-单元测试 │ │ ├── top_img.png │ │ ├── are-you-ok.jpg │ │ ├── we-vue-coverage.jpg │ │ └── ni-kai-xin-jiu-hao.jpg │ │ ├── deployer-实战经验分享 │ │ ├── top_img.jpg │ │ ├── deployer_init.png │ │ └── deployer_structure.png │ │ ├── swiper-js-loop-小坑 │ │ ├── nodes.png │ │ ├── top_img.png │ │ ├── screenshot1.png │ │ └── screenshot2.png │ │ ├── ElementUI-radio-小改造 │ │ ├── after.png │ │ ├── before.png │ │ └── top_img.jpg │ │ ├── 绳命在于折腾-我用-Nuxt-js-重构了博客 │ │ ├── top_img.png │ │ └── generate-result.png │ │ ├── Laravel-自动转换长整型雪花-ID-为字符串 │ │ ├── result.png │ │ └── top_img.jpg │ │ ├── highlight-js-在-Vue-中使用的一点儿经验 │ │ ├── 一般效果.png │ │ ├── 渲染效果.png │ │ └── top_img.jpg │ │ ├── 使用-Laravel-数据填充功能生成中文测试数据 │ │ ├── result.png │ │ └── top_img.jpg │ │ ├── 自己撸个-vue-markdown-loader │ │ ├── top_img.jpg │ │ └── markdown-loader.jpg │ │ ├── github-wakatime-x-dashboard │ │ ├── top_img.jpg │ │ ├── x-dashboard.test_github.png │ │ └── x-dashboard.test_wakatime.png │ │ ├── laravel5-5-中读写分离需要注意的一点小问题 │ │ └── top_img.jpg │ │ ├── 让-F5-歇一会儿——laravel-mix-自动刷新之道 │ │ ├── gif-bs.gif │ │ ├── gif-hmr.gif │ │ ├── top_img.jpg │ │ ├── bs-scroll-demo.gif │ │ └── gif-livereload.gif │ │ ├── 在-Laravel-项目中使用-webpack-encore │ │ └── encore.png │ │ ├── 用-Algolia-DocSearch-轻松实现文档全站搜索 │ │ ├── top_img.jpg │ │ ├── doc_search_letter.jpg │ │ ├── doc_search_result.png │ │ └── doc_search_steps.png │ │ ├── Homestead-laravel-mix-环境下-hmr-的两种玩法 │ │ ├── error.png │ │ ├── 方法1.png │ │ ├── 方法2.png │ │ └── top_img.png │ │ ├── Laravel-中使用-puppeteer-采集异步加载的网页内容 │ │ ├── top_img.png │ │ ├── toutiao.jpg │ │ ├── toutiao_log.jpg │ │ └── install_puppeteer.jpg │ │ ├── intervention-image-中的一个小坑及其破解之法 │ │ └── top_img.jpg │ │ ├── Laravel-laravel-echo-EasyWeChat-实现微信扫码登录 │ │ ├── top_img.jpg │ │ └── screenshot.gif │ │ └── 小分享——webpack-encore-laravel-helpers │ │ └── webpack-encore-laravel.jpg ├── CNAME └── favicon.ico ├── CNAME ├── .vscode └── settings.json ├── .stylelintignore ├── .env.example ├── posts ├── meta.json ├── Vue-axios-便捷提示-Loading-状态.md ├── CSS-减肥灵药——purgecss.md ├── 如何用-Vue-写一个虚拟数字键盘.md ├── laravel5-5-中读写分离需要注意的一点小问题.md ├── 使用-Laravel-数据填充功能生成中文测试数据.md ├── intervention-image-中的一个小坑及其破解之法.md ├── highlight-js-在-Vue-中使用的一点儿经验.md ├── swiper-js-loop-小坑.md ├── Laravel-自动转换长整型雪花-ID-为字符串.md ├── Laravel-中使用-puppeteer-采集异步加载的网页内容.md ├── Homestead-laravel-mix-环境下-hmr-的两种玩法.md ├── 用-Algolia-DocSearch-轻松实现文档全站搜索.md ├── ElementUI-radio-小改造.md ├── 让-F5-歇一会儿——laravel-mix-自动刷新之道.md ├── Laravel-laravel-echo-EasyWeChat-实现微信扫码登录.md ├── 绳命在于折腾-我用-Nuxt-js-重构了博客.md └── 在-Laravel-项目中使用-webpack-encore.md ├── assets ├── image │ ├── logo.png │ ├── avatar.jpg │ └── resume │ │ ├── niudaji.jpg │ │ ├── dunhuang.jpg │ │ ├── qrcode-qmyd.png │ │ ├── qrcode-fscinm.png │ │ ├── qrcode-kunshan.png │ │ ├── qrcode-dreamore.png │ │ └── qrcode-improvecn.png ├── scss │ ├── _variables.scss │ └── app.scss ├── sprite │ └── svg │ │ ├── clock.svg │ │ ├── trash.svg │ │ ├── pie-chart.svg │ │ ├── resume │ │ ├── mobile.svg │ │ ├── qq.svg │ │ ├── star-fill.svg │ │ ├── star.svg │ │ ├── github.svg │ │ ├── wechat.svg │ │ └── blog.svg │ │ ├── back.svg │ │ ├── dish.svg │ │ ├── ink-pen.svg │ │ ├── edit.svg │ │ ├── cup.svg │ │ └── chart.svg └── README.md ├── vue-shim.d.ts ├── types └── vue-shim.d.ts ├── layouts ├── basic.vue ├── error.vue └── default.vue ├── .eslintignore ├── utils └── index.ts ├── content └── posts │ ├── Vue-axios-便捷提示-Loading-状态.md │ ├── CSS-减肥灵药——purgecss.md │ ├── 如何用-Vue-写一个虚拟数字键盘.md │ ├── laravel5-5-中读写分离需要注意的一点小问题.md │ ├── 使用-Laravel-数据填充功能生成中文测试数据.md │ ├── intervention-image-中的一个小坑及其破解之法.md │ ├── highlight-js-在-Vue-中使用的一点儿经验.md │ ├── swiper-js-loop-小坑.md │ ├── Laravel-中使用-puppeteer-采集异步加载的网页内容.md │ ├── Homestead-laravel-mix-环境下-hmr-的两种玩法.md │ ├── 用-Algolia-DocSearch-轻松实现文档全站搜索.md │ ├── ElementUI-radio-小改造.md │ ├── 让-F5-歇一会儿——laravel-mix-自动刷新之道.md │ ├── Laravel-laravel-echo-EasyWeChat-实现微信扫码登录.md │ └── 绳命在于折腾-我用-Nuxt-js-重构了博客.md ├── scripts ├── tsconfig.json ├── create-post.ts └── generate-post-list.ts ├── webstorm-webpack.config.js ├── .editorconfig ├── test └── Logo.spec.js ├── .babelrc ├── mixins ├── dayjs.js ├── map-loading-state.js └── paginator.js ├── plugins ├── README.md └── app-service.ts ├── middleware └── README.md ├── store └── index.ts ├── global.d.ts ├── app.html ├── components ├── SectionTitle.vue ├── Footer.vue ├── BackToTop.vue └── Header.vue ├── jest.config.js ├── README.md ├── pages ├── resume │ ├── components │ │ ├── Education.vue │ │ ├── StarRanking.vue │ │ ├── Contact.vue │ │ ├── BasicInfo.vue │ │ ├── OpenSource.vue │ │ ├── WorkExperience.vue │ │ ├── Skills.vue │ │ └── Cases.vue │ └── index.vue ├── open-source.vue ├── posts │ └── _title.vue └── index.vue ├── tsconfig.json ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .eslintrc.js ├── tailwind.config.js ├── package.json ├── nuxt.config.ts └── stylelint.config.js /constants/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /static/images/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | tianyong90.com 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /static/CNAME: -------------------------------------------------------------------------------- 1 | tianyong90.com 2 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .nuxt/ 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # github personal access token 2 | GIT_TOKEN= 3 | -------------------------------------------------------------------------------- /posts/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "updatedAt": "2020-12-28 13:00:09" 3 | } 4 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /assets/image/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/assets/image/logo.png -------------------------------------------------------------------------------- /assets/image/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/assets/image/avatar.jpg -------------------------------------------------------------------------------- /assets/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $color-primary: #717a5c; 2 | 3 | $color-oriental-pink: #c79d88; 4 | -------------------------------------------------------------------------------- /vue-shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /types/vue-shim.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /assets/image/resume/niudaji.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/assets/image/resume/niudaji.jpg -------------------------------------------------------------------------------- /assets/image/resume/dunhuang.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/assets/image/resume/dunhuang.jpg -------------------------------------------------------------------------------- /assets/image/resume/qrcode-qmyd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/assets/image/resume/qrcode-qmyd.png -------------------------------------------------------------------------------- /layouts/basic.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | -------------------------------------------------------------------------------- /assets/image/resume/qrcode-fscinm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/assets/image/resume/qrcode-fscinm.png -------------------------------------------------------------------------------- /assets/image/resume/qrcode-kunshan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/assets/image/resume/qrcode-kunshan.png -------------------------------------------------------------------------------- /assets/image/resume/qrcode-dreamore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/assets/image/resume/qrcode-dreamore.png -------------------------------------------------------------------------------- /assets/image/resume/qrcode-improvecn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/assets/image/resume/qrcode-improvecn.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | # nuxt.js build output 4 | .nuxt 5 | 6 | # Nuxt generate 7 | dist 8 | 9 | /scripts/*.js 10 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | export function fixedEncodeURI(str: string): string { 2 | return encodeURI(str).replace(/%5B/g, '[').replace(/%5D/g, ']') 3 | } 4 | -------------------------------------------------------------------------------- /static/images/post-images/也来扯扯-Vue-单元测试/top_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/也来扯扯-Vue-单元测试/top_img.png -------------------------------------------------------------------------------- /static/images/post-images/deployer-实战经验分享/top_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/deployer-实战经验分享/top_img.jpg -------------------------------------------------------------------------------- /static/images/post-images/swiper-js-loop-小坑/nodes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/swiper-js-loop-小坑/nodes.png -------------------------------------------------------------------------------- /static/images/post-images/ElementUI-radio-小改造/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/ElementUI-radio-小改造/after.png -------------------------------------------------------------------------------- /static/images/post-images/ElementUI-radio-小改造/before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/ElementUI-radio-小改造/before.png -------------------------------------------------------------------------------- /static/images/post-images/swiper-js-loop-小坑/top_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/swiper-js-loop-小坑/top_img.png -------------------------------------------------------------------------------- /static/images/post-images/也来扯扯-Vue-单元测试/are-you-ok.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/也来扯扯-Vue-单元测试/are-you-ok.jpg -------------------------------------------------------------------------------- /static/images/post-images/ElementUI-radio-小改造/top_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/ElementUI-radio-小改造/top_img.jpg -------------------------------------------------------------------------------- /static/images/post-images/deployer-实战经验分享/deployer_init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/deployer-实战经验分享/deployer_init.png -------------------------------------------------------------------------------- /static/images/post-images/swiper-js-loop-小坑/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/swiper-js-loop-小坑/screenshot1.png -------------------------------------------------------------------------------- /static/images/post-images/swiper-js-loop-小坑/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/swiper-js-loop-小坑/screenshot2.png -------------------------------------------------------------------------------- /static/images/post-images/也来扯扯-Vue-单元测试/we-vue-coverage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/也来扯扯-Vue-单元测试/we-vue-coverage.jpg -------------------------------------------------------------------------------- /static/images/post-images/绳命在于折腾-我用-Nuxt-js-重构了博客/top_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/绳命在于折腾-我用-Nuxt-js-重构了博客/top_img.png -------------------------------------------------------------------------------- /static/images/post-images/Laravel-自动转换长整型雪花-ID-为字符串/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/Laravel-自动转换长整型雪花-ID-为字符串/result.png -------------------------------------------------------------------------------- /static/images/post-images/Laravel-自动转换长整型雪花-ID-为字符串/top_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/Laravel-自动转换长整型雪花-ID-为字符串/top_img.jpg -------------------------------------------------------------------------------- /static/images/post-images/highlight-js-在-Vue-中使用的一点儿经验/一般效果.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/highlight-js-在-Vue-中使用的一点儿经验/一般效果.png -------------------------------------------------------------------------------- /static/images/post-images/highlight-js-在-Vue-中使用的一点儿经验/渲染效果.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/highlight-js-在-Vue-中使用的一点儿经验/渲染效果.png -------------------------------------------------------------------------------- /static/images/post-images/也来扯扯-Vue-单元测试/ni-kai-xin-jiu-hao.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/也来扯扯-Vue-单元测试/ni-kai-xin-jiu-hao.jpg -------------------------------------------------------------------------------- /static/images/post-images/使用-Laravel-数据填充功能生成中文测试数据/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/使用-Laravel-数据填充功能生成中文测试数据/result.png -------------------------------------------------------------------------------- /static/images/post-images/使用-Laravel-数据填充功能生成中文测试数据/top_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/使用-Laravel-数据填充功能生成中文测试数据/top_img.jpg -------------------------------------------------------------------------------- /static/images/post-images/自己撸个-vue-markdown-loader/top_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/自己撸个-vue-markdown-loader/top_img.jpg -------------------------------------------------------------------------------- /static/images/post-images/deployer-实战经验分享/deployer_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/deployer-实战经验分享/deployer_structure.png -------------------------------------------------------------------------------- /static/images/post-images/github-wakatime-x-dashboard/top_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/github-wakatime-x-dashboard/top_img.jpg -------------------------------------------------------------------------------- /static/images/post-images/highlight-js-在-Vue-中使用的一点儿经验/top_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/highlight-js-在-Vue-中使用的一点儿经验/top_img.jpg -------------------------------------------------------------------------------- /static/images/post-images/laravel5-5-中读写分离需要注意的一点小问题/top_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/laravel5-5-中读写分离需要注意的一点小问题/top_img.jpg -------------------------------------------------------------------------------- /static/images/post-images/让-F5-歇一会儿——laravel-mix-自动刷新之道/gif-bs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/让-F5-歇一会儿——laravel-mix-自动刷新之道/gif-bs.gif -------------------------------------------------------------------------------- /static/images/post-images/在-Laravel-项目中使用-webpack-encore/encore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/在-Laravel-项目中使用-webpack-encore/encore.png -------------------------------------------------------------------------------- /static/images/post-images/用-Algolia-DocSearch-轻松实现文档全站搜索/top_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/用-Algolia-DocSearch-轻松实现文档全站搜索/top_img.jpg -------------------------------------------------------------------------------- /static/images/post-images/让-F5-歇一会儿——laravel-mix-自动刷新之道/gif-hmr.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/让-F5-歇一会儿——laravel-mix-自动刷新之道/gif-hmr.gif -------------------------------------------------------------------------------- /static/images/post-images/让-F5-歇一会儿——laravel-mix-自动刷新之道/top_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/让-F5-歇一会儿——laravel-mix-自动刷新之道/top_img.jpg -------------------------------------------------------------------------------- /static/images/post-images/Homestead-laravel-mix-环境下-hmr-的两种玩法/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/Homestead-laravel-mix-环境下-hmr-的两种玩法/error.png -------------------------------------------------------------------------------- /static/images/post-images/Homestead-laravel-mix-环境下-hmr-的两种玩法/方法1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/Homestead-laravel-mix-环境下-hmr-的两种玩法/方法1.png -------------------------------------------------------------------------------- /static/images/post-images/Homestead-laravel-mix-环境下-hmr-的两种玩法/方法2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/Homestead-laravel-mix-环境下-hmr-的两种玩法/方法2.png -------------------------------------------------------------------------------- /static/images/post-images/Laravel-中使用-puppeteer-采集异步加载的网页内容/top_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/Laravel-中使用-puppeteer-采集异步加载的网页内容/top_img.png -------------------------------------------------------------------------------- /static/images/post-images/Laravel-中使用-puppeteer-采集异步加载的网页内容/toutiao.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/Laravel-中使用-puppeteer-采集异步加载的网页内容/toutiao.jpg -------------------------------------------------------------------------------- /static/images/post-images/intervention-image-中的一个小坑及其破解之法/top_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/intervention-image-中的一个小坑及其破解之法/top_img.jpg -------------------------------------------------------------------------------- /static/images/post-images/绳命在于折腾-我用-Nuxt-js-重构了博客/generate-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/绳命在于折腾-我用-Nuxt-js-重构了博客/generate-result.png -------------------------------------------------------------------------------- /static/images/post-images/自己撸个-vue-markdown-loader/markdown-loader.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/自己撸个-vue-markdown-loader/markdown-loader.jpg -------------------------------------------------------------------------------- /static/images/post-images/Homestead-laravel-mix-环境下-hmr-的两种玩法/top_img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/Homestead-laravel-mix-环境下-hmr-的两种玩法/top_img.png -------------------------------------------------------------------------------- /static/images/post-images/Laravel-中使用-puppeteer-采集异步加载的网页内容/toutiao_log.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/Laravel-中使用-puppeteer-采集异步加载的网页内容/toutiao_log.jpg -------------------------------------------------------------------------------- /static/images/post-images/让-F5-歇一会儿——laravel-mix-自动刷新之道/bs-scroll-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/让-F5-歇一会儿——laravel-mix-自动刷新之道/bs-scroll-demo.gif -------------------------------------------------------------------------------- /static/images/post-images/让-F5-歇一会儿——laravel-mix-自动刷新之道/gif-livereload.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/让-F5-歇一会儿——laravel-mix-自动刷新之道/gif-livereload.gif -------------------------------------------------------------------------------- /static/images/post-images/Laravel-laravel-echo-EasyWeChat-实现微信扫码登录/top_img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/Laravel-laravel-echo-EasyWeChat-实现微信扫码登录/top_img.jpg -------------------------------------------------------------------------------- /static/images/post-images/用-Algolia-DocSearch-轻松实现文档全站搜索/doc_search_letter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/用-Algolia-DocSearch-轻松实现文档全站搜索/doc_search_letter.jpg -------------------------------------------------------------------------------- /static/images/post-images/用-Algolia-DocSearch-轻松实现文档全站搜索/doc_search_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/用-Algolia-DocSearch-轻松实现文档全站搜索/doc_search_result.png -------------------------------------------------------------------------------- /static/images/post-images/用-Algolia-DocSearch-轻松实现文档全站搜索/doc_search_steps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/用-Algolia-DocSearch-轻松实现文档全站搜索/doc_search_steps.png -------------------------------------------------------------------------------- /static/images/post-images/Laravel-laravel-echo-EasyWeChat-实现微信扫码登录/screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/Laravel-laravel-echo-EasyWeChat-实现微信扫码登录/screenshot.gif -------------------------------------------------------------------------------- /static/images/post-images/Laravel-中使用-puppeteer-采集异步加载的网页内容/install_puppeteer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/Laravel-中使用-puppeteer-采集异步加载的网页内容/install_puppeteer.jpg -------------------------------------------------------------------------------- /static/images/post-images/github-wakatime-x-dashboard/x-dashboard.test_github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/github-wakatime-x-dashboard/x-dashboard.test_github.png -------------------------------------------------------------------------------- /static/images/post-images/github-wakatime-x-dashboard/x-dashboard.test_wakatime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/github-wakatime-x-dashboard/x-dashboard.test_wakatime.png -------------------------------------------------------------------------------- /static/images/post-images/小分享——webpack-encore-laravel-helpers/webpack-encore-laravel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tianyong90/blog/HEAD/static/images/post-images/小分享——webpack-encore-laravel-helpers/webpack-encore-laravel.jpg -------------------------------------------------------------------------------- /posts/Vue-axios-便捷提示-Loading-状态.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Vue + axios, 便捷提示 Loading 状态' 3 | date: '2020-11-We 11:24:25' 4 | top_img: ./top_img.png 5 | tags: 6 | - '' 7 | categories: 8 | - '' 9 | draft: true 10 | --- 11 | 12 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | . "$(dirname "$0")/common.sh" 4 | 5 | [ -n "$CI" ] && exit 0 6 | 7 | # Format and submit code according to lintstagedrc.js configuration 8 | npm run lint:lint-staged 9 | -------------------------------------------------------------------------------- /.husky/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | command_exists () { 3 | command -v "$1" >/dev/null 2>&1 4 | } 5 | 6 | # Workaround for Windows 10, Git Bash and Yarn 7 | if command_exists winpty && test -t 1; then 8 | exec < /dev/tty 9 | fi 10 | -------------------------------------------------------------------------------- /content/posts/Vue-axios-便捷提示-Loading-状态.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Vue + axios, 便捷提示 Loading 状态' 3 | date: '2020-11-We 11:24:25' 4 | top_img: /top_img.png 5 | tags: 6 | - '' 7 | categories: 8 | - '' 9 | draft: true 10 | --- 11 | 12 | -------------------------------------------------------------------------------- /layouts/error.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ error.statusCode }} 4 | 5 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "commonjs", 6 | "baseUrl": "." 7 | }, 8 | "include": [ 9 | "./*.ts", 10 | "../global.d.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /webstorm-webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 此文件用于 webstorm 配置,以便 webstorm 能识别 webpack 设置的路径别名 3 | */ 4 | 5 | const path = require('path') 6 | 7 | module.exports = { 8 | resolve: { 9 | alias: { 10 | '@': path.resolve(__dirname), 11 | }, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /test/Logo.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import Logo from '@/components/Logo.vue' 3 | 4 | describe('Logo', () => { 5 | test('is a Vue instance', () => { 6 | const wrapper = mount(Logo) 7 | expect(wrapper.isVueInstance()).toBeTruthy() 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "targets": { 9 | "node": "current" 10 | } 11 | } 12 | ] 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /assets/sprite/svg/clock.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /mixins/dayjs.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | export default { 4 | filters: { 5 | friendlyTime(value, format = 'YYYY-MM-DD HH:mm:ss') { 6 | if (value && dayjs(value).isValid()) { 7 | return dayjs(value).format(format) 8 | } 9 | return '-' 10 | }, 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /mixins/map-loading-state.js: -------------------------------------------------------------------------------- 1 | import { mapState } from 'vuex' 2 | 3 | export default { 4 | computed: { 5 | ...mapState({ 6 | isLoading: state => state.axios.isLoading, 7 | isSubmitting: state => state.axios.isSubmitting, 8 | axiosTriggerId: state => state.axios.triggerId, 9 | }), 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # ASSETS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your un-compiled assets such as LESS, SASS, or JavaScript. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/assets#webpacked). 8 | -------------------------------------------------------------------------------- /assets/sprite/svg/trash.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # PLUGINS 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains Javascript plugins that you want to run before mounting the root Vue.js application. 6 | 7 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/plugins). 8 | -------------------------------------------------------------------------------- /.husky/lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{js,jsx,ts,tsx}': ['eslint --fix'], 3 | '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'], 4 | 'package.json': ['prettier --write'], 5 | '*.vue': ['eslint --fix', 'stylelint --fix'], 6 | '*.{scss,less,styl,html}': ['stylelint --fix'], 7 | '*.md': ['prettier --write'], 8 | }; 9 | -------------------------------------------------------------------------------- /assets/sprite/svg/pie-chart.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /middleware/README.md: -------------------------------------------------------------------------------- 1 | # MIDDLEWARE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your application middleware. 6 | Middleware let you define custom functions that can be run before rendering either a page or a group of pages. 7 | 8 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/routing#middleware). 9 | -------------------------------------------------------------------------------- /store/index.ts: -------------------------------------------------------------------------------- 1 | export const state = () => ({ 2 | dropdownMenuVisible: false, 3 | postCount: 0, 4 | updatedAt: '', 5 | }) 6 | 7 | export const mutations = { 8 | UPDATE_DROPDOWN_MENU_VISIBLE(state, value: boolean) { 9 | state.dropdownMenuVisible = value 10 | }, 11 | 12 | UPDATE_POST_COUNT(state, value: boolean) { 13 | state.postCount = value 14 | }, 15 | 16 | UPDATE_UPDATED_AT(state, value: boolean) { 17 | state.updatedAt = value 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vue/types/vue' { 2 | export interface Vue { 3 | $hello: string 4 | } 5 | } 6 | 7 | declare module 'blog/types' { 8 | export type Post = { 9 | filename: string 10 | slugifiedFilename: string 11 | title: string 12 | date: string 13 | /* eslint-disable-next-line */ 14 | top_img: string 15 | tags: Array 16 | categories: Array 17 | description: string 18 | draft?: boolean 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /mixins/paginator.js: -------------------------------------------------------------------------------- 1 | export default { 2 | created() { 3 | if (this.query.page) { 4 | // paginator current_page 绑定必须为 number 5 | this.query.page = parseInt(this.query.page) 6 | } 7 | 8 | if (this.query.per_page) { 9 | // paginator page_size 绑定必须为 number 10 | this.query.per_page = parseInt(this.query.per_page) 11 | } 12 | }, 13 | 14 | computed: { 15 | paginatorVisible() { 16 | return this.items.data.length > 0 17 | }, 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ HEAD }} 5 | 6 | 7 | {{ APP }} 8 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /components/SectionTitle.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 14 | 15 | 26 | -------------------------------------------------------------------------------- /assets/sprite/svg/resume/mobile.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | '^@/(.*)$': '/$1', 4 | '^~/(.*)$': '/$1', 5 | '^vue$': 'vue/dist/vue.common.js' 6 | }, 7 | moduleFileExtensions: [ 8 | 'ts', 9 | 'js', 10 | 'vue', 11 | 'json' 12 | ], 13 | transform: { 14 | '^.+\\.ts$': 'ts-jest', 15 | '^.+\\.js$': 'babel-jest', 16 | '.*\\.(vue)$': 'vue-jest' 17 | }, 18 | collectCoverage: true, 19 | collectCoverageFrom: [ 20 | '/components/**/*.vue', 21 | '/pages/**/*.vue' 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 田勇的博客 2 | 3 |  4 | 5 | ## 博客地址,[https://tianyong90.com](https://tianyong90.com) 6 | 7 | ## Build Setup 8 | 9 | ``` bash 10 | # install dependencies 11 | $ yarn install 12 | 13 | # serve with hot reload at localhost:3000 14 | $ yarn run dev 15 | 16 | # build for production and launch server 17 | $ yarn run build 18 | $ yarn start 19 | 20 | # generate static project 21 | $ yarn run generate 22 | ``` 23 | 24 | For detailed explanation on how things work, checkout [Nuxt.js docs](https://nuxtjs.org). 25 | -------------------------------------------------------------------------------- /components/Footer.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 32 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 34 | -------------------------------------------------------------------------------- /assets/sprite/svg/resume/qq.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /assets/sprite/svg/resume/star-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /pages/resume/components/Education.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 教育经历 5 | 6 | 7 | 12 | {{ item.duration }} 13 | {{ item.name }} 14 | 15 | 16 | 17 | 18 | 19 | 38 | 39 | 41 | -------------------------------------------------------------------------------- /plugins/app-service.ts: -------------------------------------------------------------------------------- 1 | import { Post } from 'blog/types' 2 | import posts from '~/posts/posts.json' 3 | import metaData from '~/posts/meta.json' 4 | 5 | export default ({ app, store }) => { 6 | // 过滤掉草稿 7 | const publishedPosts = (posts as Array).filter((post) => { 8 | return !post.draft 9 | }) 10 | 11 | store.commit('UPDATE_POST_COUNT', publishedPosts.length) 12 | store.commit('UPDATE_UPDATED_AT', metaData.updatedAt) 13 | 14 | const datetime = new Date() 15 | 16 | if (datetime.getFullYear() === 2020 && datetime.getMonth() === 3 && datetime.getDate() === 4) { 17 | window.document.body.classList.add('grayscale') 18 | } 19 | 20 | app.router.beforeEach((to, from, next) => { 21 | // 页面切换时收起下拉菜单 22 | store.commit('UPDATE_DROPDOWN_MENU_VISIBLE', false) 23 | 24 | next() 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /assets/sprite/svg/resume/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "lib": [ 7 | "ESNext", 8 | "ESNext.AsyncIterable", 9 | "DOM" 10 | ], 11 | "esModuleInterop": true, 12 | "allowJs": true, 13 | "sourceMap": true, 14 | "strict": false, 15 | "noEmit": true, 16 | "experimentalDecorators": true, 17 | "allowSyntheticDefaultImports": true, 18 | "resolveJsonModule": true, 19 | "baseUrl": ".", 20 | "paths": { 21 | "~/*": [ 22 | "./*" 23 | ], 24 | "@/*": [ 25 | "./*" 26 | ] 27 | }, 28 | "types": [ 29 | "@types/node", 30 | "@nuxt/types", 31 | "@nuxt/content" 32 | ] 33 | }, 34 | "exclude": [ 35 | "node_modules", 36 | ".nuxt", 37 | "dist" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /assets/sprite/svg/back.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /assets/sprite/svg/resume/github.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /pages/resume/components/StarRanking.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 33 | 34 | 35 | 46 | -------------------------------------------------------------------------------- /posts/CSS-减肥灵药——purgecss.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSS 减肥灵药——purgecss 3 | date: '2019-12-27 05:34:49' 4 | top_img: '' 5 | tags: 6 | - 'css' 7 | - 'purgecss' 8 | categories: 9 | - '前端' 10 | draft: true 11 | --- 12 | 13 | ## 缘起 14 | 15 | 第一次看到 purgecss 这一名字,是在 laravel-mix 的文档中,但当时并未心动。那时的我还只会精简优化打包的 JS 代码,并没有意识到 CSS 也有很大的瘦身空间。直到后来再一次在 tailwindcss 的文档里看到对 purgecss 的介绍。 16 | 17 | 18 | 19 | ## 基本配置 20 | 21 | purgecss 通常与一些插件或者构建工具配合使用,例如与 postcss 配合,或者安装 webpack 相关的插件来使用。个人认为与 postcss 配合是最为简单快捷的用法。因此,下面的配置也以此种使用场景为例。 22 | 23 | 24 | 25 | ```js 26 | module.exports = { 27 | 28 | } 29 | ``` 30 | 31 | 32 | 33 | ## 效果对比(tailwindcss) 34 | 35 | tailwindcss 是对 purgecss 支持非常好的项目之一,它的官方文档是明确指出了可以利用 purgecss 大幅度精简构建后的体积。按照文档中的配置进行试验。在仅使用 margin padding 等几个常用类的情况下,purgecss 的瘦身效果相当明显。 36 | 37 | 38 | 39 | ## 白名单使用以及注意事项 40 | 41 | 42 | ## 关于开发第三组件或插件的建议 43 | 44 | 如果你正在开发可复用的组件库或者插件,不管是 Vue 还是 React,在组件样式时,一定要使用一个统一而且尽量与其它主流组件/插件库不同的前缀。虽然这表面上可能会使 CSS 类名变长,却能为在构建时使用 purgecss 带来方便。而且对于使用 scss 等 CSS 预处理语言时而言,添加这类统一前缀是十分方便实现的。至于直接手撸 CSS 的情况,也无非就是复制粘贴一把梭。 45 | 46 | 47 | 48 | ## 结语 49 | 50 | -------------------------------------------------------------------------------- /content/posts/CSS-减肥灵药——purgecss.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CSS 减肥灵药——purgecss 3 | date: '2019-12-27 05:34:49' 4 | top_img: '' 5 | tags: 6 | - 'css' 7 | - 'purgecss' 8 | categories: 9 | - '前端' 10 | draft: true 11 | --- 12 | 13 | ## 缘起 14 | 15 | 第一次看到 purgecss 这一名字,是在 laravel-mix 的文档中,但当时并未心动。那时的我还只会精简优化打包的 JS 代码,并没有意识到 CSS 也有很大的瘦身空间。直到后来再一次在 tailwindcss 的文档里看到对 purgecss 的介绍。 16 | 17 | 18 | 19 | ## 基本配置 20 | 21 | purgecss 通常与一些插件或者构建工具配合使用,例如与 postcss 配合,或者安装 webpack 相关的插件来使用。个人认为与 postcss 配合是最为简单快捷的用法。因此,下面的配置也以此种使用场景为例。 22 | 23 | 24 | 25 | ```js 26 | module.exports = { 27 | 28 | } 29 | ``` 30 | 31 | 32 | 33 | ## 效果对比(tailwindcss) 34 | 35 | tailwindcss 是对 purgecss 支持非常好的项目之一,它的官方文档是明确指出了可以利用 purgecss 大幅度精简构建后的体积。按照文档中的配置进行试验。在仅使用 margin padding 等几个常用类的情况下,purgecss 的瘦身效果相当明显。 36 | 37 | 38 | 39 | ## 白名单使用以及注意事项 40 | 41 | 42 | ## 关于开发第三组件或插件的建议 43 | 44 | 如果你正在开发可复用的组件库或者插件,不管是 Vue 还是 React,在组件样式时,一定要使用一个统一而且尽量与其它主流组件/插件库不同的前缀。虽然这表面上可能会使 CSS 类名变长,却能为在构建时使用 purgecss 带来方便。而且对于使用 scss 等 CSS 预处理语言时而言,添加这类统一前缀是十分方便实现的。至于直接手撸 CSS 的情况,也无非就是复制粘贴一把梭。 45 | 46 | 47 | 48 | ## 结语 49 | 50 | -------------------------------------------------------------------------------- /posts/如何用-Vue-写一个虚拟数字键盘.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 如何用 Vue 写一个虚拟数字键盘 3 | date: '2020-03-Su 23:14:35' 4 | top_img: ./top_img.png 5 | tags: 6 | - vue 7 | - 数字键盘 8 | categories: 9 | - 前端 10 | draft: true 11 | --- 12 | 13 | 数字键盘,应用场景非常广泛,尤其在手机端,很多时候我们需要输入手机号、输入短信验证码或者输入支付金额。通常情况下,只需要使用 number 类型的 input,同时辅以适当的表单验证逻辑即可。 14 | 15 | 但这种方法虽然简单,但不管对于编程开发还是用户体验来说并不是很好。最主要理由如下: 16 | 17 | 1. 对于不同系统、不同输入法,数字键盘视觉和键位布局上并不统一 18 | 2. 即使将 input 的 type 属性指定为 number,用户仍然能自己切换为其它键盘也算是一种误操作(因为 number input 内无法输入中英文等字符,只能切为数字键盘才能正常输入) 19 | 3. 有些情况下我们不允许输入小数点和算术符号,但手机输入法原生数字键盘总包含一些多余的符号按键而且无法隐藏,因此需要额外的处理来限制这些键输入 20 | 4. 在 iOS 中,弹出的原生键盘,可能影响 fixed 元素定位 21 | 22 | 如果使用虚拟的数据键盘,上面这些问题了就迎刃而解了。虽然目前已经有不少第三方面数字键盘组件,但我还是深度自己写了一下,主要是为了与自己项目的风格更加统一,二来方便自己设计键位布局。 23 | 24 | ## 明确按键布局和外观,写出界面 25 | 26 | 首先需要根据自己的实际应用场景确定需要哪些按键,如何进行布局。比如在我的应用里,是为了输入正整数型的礼物数量,所以除了 0 到 9 数字键之外 ,还需要删除键和确定键,此外为了方便还加了一个 ‘00’ 键。 27 | 28 | 键盘的布局可以用 table 或者 grid 来实现,当然 flex 之类的布局也是可以的。出于兼容性考虑,我选择了 table。 29 | 30 | ## 确定交互细节,完成操作逻辑部分代码 31 | 32 | 在自己的应用中,输入的是正整数,所以开头不能输入一个或多个零,同时限制输入的最大值为 9999。 33 | 34 | ## 优化视觉效果和操作体验 35 | 36 | 300ms 延迟问题 37 | 38 | 防止点击 input 弹出原生键盘 39 | 40 | 点按按键时的视觉效果 41 | -------------------------------------------------------------------------------- /content/posts/如何用-Vue-写一个虚拟数字键盘.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 如何用 Vue 写一个虚拟数字键盘 3 | date: '2020-03-Su 23:14:35' 4 | top_img: /top_img.png 5 | tags: 6 | - vue 7 | - 数字键盘 8 | categories: 9 | - 前端 10 | draft: true 11 | --- 12 | 13 | 数字键盘,应用场景非常广泛,尤其在手机端,很多时候我们需要输入手机号、输入短信验证码或者输入支付金额。通常情况下,只需要使用 number 类型的 input,同时辅以适当的表单验证逻辑即可。 14 | 15 | 但这种方法虽然简单,但不管对于编程开发还是用户体验来说并不是很好。最主要理由如下: 16 | 17 | 1. 对于不同系统、不同输入法,数字键盘视觉和键位布局上并不统一 18 | 2. 即使将 input 的 type 属性指定为 number,用户仍然能自己切换为其它键盘也算是一种误操作(因为 number input 内无法输入中英文等字符,只能切为数字键盘才能正常输入) 19 | 3. 有些情况下我们不允许输入小数点和算术符号,但手机输入法原生数字键盘总包含一些多余的符号按键而且无法隐藏,因此需要额外的处理来限制这些键输入 20 | 4. 在 iOS 中,弹出的原生键盘,可能影响 fixed 元素定位 21 | 22 | 如果使用虚拟的数据键盘,上面这些问题了就迎刃而解了。虽然目前已经有不少第三方面数字键盘组件,但我还是深度自己写了一下,主要是为了与自己项目的风格更加统一,二来方便自己设计键位布局。 23 | 24 | ## 明确按键布局和外观,写出界面 25 | 26 | 首先需要根据自己的实际应用场景确定需要哪些按键,如何进行布局。比如在我的应用里,是为了输入正整数型的礼物数量,所以除了 0 到 9 数字键之外 ,还需要删除键和确定键,此外为了方便还加了一个 ‘00’ 键。 27 | 28 | 键盘的布局可以用 table 或者 grid 来实现,当然 flex 之类的布局也是可以的。出于兼容性考虑,我选择了 table。 29 | 30 | ## 确定交互细节,完成操作逻辑部分代码 31 | 32 | 在自己的应用中,输入的是正整数,所以开头不能输入一个或多个零,同时限制输入的最大值为 9999。 33 | 34 | ## 优化视觉效果和操作体验 35 | 36 | 300ms 延迟问题 37 | 38 | 防止点击 input 弹出原生键盘 39 | 40 | 点按按键时的视觉效果 41 | -------------------------------------------------------------------------------- /pages/resume/components/Contact.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | https://github.com/tianyong90 13 | 14 | 15 | 16 | 20 | 21 | https://tianyong90.com/ 25 | 26 | 27 | 28 | 29 | 30 | 35 | 36 | 49 | -------------------------------------------------------------------------------- /scripts/create-post.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | import yargs from 'yargs' 4 | import matter from 'gray-matter' 5 | import dayjs from 'dayjs' 6 | 7 | const args: any = yargs 8 | .option('title', { 9 | string: true, 10 | demand: true, 11 | description: '标题', 12 | alias: 't', 13 | }) 14 | .option('draft', { 15 | boolean: true, 16 | default: true, 17 | description: '是否作为草稿', 18 | alias: 'd', 19 | }).argv 20 | 21 | const { title, draft } = args 22 | 23 | if (!title) { 24 | throw new Error('标题不能为空') 25 | } 26 | 27 | // eslint-disable-next-line 28 | const rControl = /[\u0000-\u001f]/g 29 | const rSpecial = /[\s~`!@#$%^&*()\-_+=[\]{}|\\;:"'<>,.?/]+/g 30 | 31 | // 标题空格转 - 32 | const slugTitle = title.replace(rControl, '').replace(rSpecial, '-') 33 | 34 | // 生成目录 35 | fs.mkdirp(`./posts/${slugTitle}`) 36 | 37 | const frontmatterData = { 38 | title, 39 | date: dayjs().format('YYYY-MM-dd HH:mm:ss'), 40 | top_img: './top_img.png', 41 | tags: [''], 42 | categories: [''], 43 | draft, 44 | } 45 | 46 | // 生成 front-matter yml 格式内容 47 | const mdContent = matter.stringify('', frontmatterData) 48 | 49 | // 生成 markdown 文件 50 | fs.writeFile(`./posts/${slugTitle}.md`, mdContent) 51 | 52 | console.info('新建成功') 53 | -------------------------------------------------------------------------------- /assets/sprite/svg/dish.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | with: 12 | persist-credentials: false 13 | - uses: actions/setup-node@v1 14 | with: 15 | node-version: '16.x' 16 | registry-url: registry.npmjs.org 17 | 18 | - name: Get yarn cache directory path 19 | id: yarn-cache-dir-path 20 | run: echo "::set-output name=dir::$(yarn cache dir)" 21 | 22 | - uses: actions/cache@v2 23 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 24 | with: 25 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 26 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-yarn- 29 | - run: yarn install 30 | 31 | - name: Generate static site 32 | env: 33 | GIT_TOKEN: "${{ secrets.GIT_TOKEN }}" 34 | run: yarn generate 35 | 36 | - name: Deploy to GitHub Pages 37 | uses: JamesIves/github-pages-deploy-action@3.5.5 38 | with: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | BRANCH: gh-pages # The branch the action should deploy to. 41 | FOLDER: dist # The folder the action should deploy. 42 | 43 | -------------------------------------------------------------------------------- /components/BackToTop.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 47 | 48 | 71 | -------------------------------------------------------------------------------- /assets/sprite/svg/ink-pen.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/sprite/svg/resume/wechat.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /posts/laravel5-5-中读写分离需要注意的一点小问题.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: laravel5.5 中读写分离需要注意的一点小问题 3 | date: 2019-03-10 16:41:04 4 | top_img: ./top_img.jpg 5 | tags: 6 | - laravel 7 | - 读写分离 8 | categories: 9 | - php 10 | - Laravel 11 | --- 12 | Laravel5.5 是 Laravel 最新的一个 LTS 版本,发布至今已有些时日,眼看着 5.6 都快出来了,最近终于下手将公司项目从 Laravel5.2 升级到 5.5。 13 | 14 | 因为跨了好几个版本,变化不少,加上其它一些不兼容的包也得相应作调整并进行测试,前后两天折腾下来总算弄完。上线后一切正常,似乎连运行速度都提高了不少(可能只是心理作用 :smile:)。 15 | 16 | 然而没多久出现了一种奇怪的现象,明明刚刚写入了数据,但查询时却报 No query result ,而且只是偶然性出现,没啥规律。自己直接连上数据库一查,里面明明白白的记录摆在那儿,难道见鬼了不成? 17 | 18 | 起初以为是 prettus/l5-repository 包的缓存引起的,但关掉它的缓存功能后问题依旧。后来好一阵折腾,直到再一次仔细翻看文档, 才发现 Laravel5.5 数据库读写分离配置的部分额外提到了一个 sticky 项,文档里这部分原文如下: 19 | 20 | > The sticky Option 21 | 22 | > The sticky option is an optional value that can be used to allow the immediate reading of records that have been written to the database during the current request cycle. If the sticky option is enabled and a "write" operation has been performed against the database during the current request cycle, any further "read" operations will use the "write" connection. This ensures that any data written during the request cycle can be immediately read back from the database during that same request. It is up to you to decide if this is the desired behavior for your application. 23 | 24 | 所以情况一下就明朗了,在没有启用 sticky 的时候,使用 write 连接写入数据后**立即**读取,读取时使用的是 read 连接,这样就有可能出问题。将 sticky 设置为 true 后,在与这个写入操作相同的请求周期内的后续读取操作,仍然使用原来的 write 连接,就不会有这麻烦了。 25 | 26 | 对比过早前版本的文档后发现,sticky 配置项确实是在 laravel5.5 文档里首次出现。但仅仅是在数据库配置的章节里,版本升级指南中却没有提到。对于从旧版本升级来的用户,就很有可能入这坑了…… 27 | -------------------------------------------------------------------------------- /content/posts/laravel5-5-中读写分离需要注意的一点小问题.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: laravel5.5 中读写分离需要注意的一点小问题 3 | date: 2019-03-10 16:41:04 4 | top_img: /top_img.jpg 5 | tags: 6 | - laravel 7 | - 读写分离 8 | categories: 9 | - php 10 | - Laravel 11 | draft: false 12 | --- 13 | 14 | Laravel5.5 是 Laravel 最新的一个 LTS 版本,发布至今已有些时日,眼看着 5.6 都快出来了,最近终于下手将公司项目从 Laravel5.2 升级到 5.5。 15 | 16 | 因为跨了好几个版本,变化不少,加上其它一些不兼容的包也得相应作调整并进行测试,前后两天折腾下来总算弄完。上线后一切正常,似乎连运行速度都提高了不少(可能只是心理作用 :smile:)。 17 | 18 | 然而没多久出现了一种奇怪的现象,明明刚刚写入了数据,但查询时却报 No query result ,而且只是偶然性出现,没啥规律。自己直接连上数据库一查,里面明明白白的记录摆在那儿,难道见鬼了不成? 19 | 20 | 起初以为是 prettus/l5-repository 包的缓存引起的,但关掉它的缓存功能后问题依旧。后来好一阵折腾,直到再一次仔细翻看文档, 才发现 Laravel5.5 数据库读写分离配置的部分额外提到了一个 sticky 项,文档里这部分原文如下: 21 | 22 | > The sticky Option 23 | 24 | > The sticky option is an optional value that can be used to allow the immediate reading of records that have been written to the database during the current request cycle. If the sticky option is enabled and a "write" operation has been performed against the database during the current request cycle, any further "read" operations will use the "write" connection. This ensures that any data written during the request cycle can be immediately read back from the database during that same request. It is up to you to decide if this is the desired behavior for your application. 25 | 26 | 所以情况一下就明朗了,在没有启用 sticky 的时候,使用 write 连接写入数据后**立即**读取,读取时使用的是 read 连接,这样就有可能出问题。将 sticky 设置为 true 后,在与这个写入操作相同的请求周期内的后续读取操作,仍然使用原来的 write 连接,就不会有这麻烦了。 27 | 28 | 对比过早前版本的文档后发现,sticky 配置项确实是在 laravel5.5 文档里首次出现。但仅仅是在数据库配置的章节里,版本升级指南中却没有提到。对于从旧版本升级来的用户,就很有可能入这坑了…… 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | -------------------------------------------------------------------------------- /scripts/generate-post-list.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import fs from 'fs-extra' 3 | import matter from 'gray-matter' 4 | import summarize from 'summarize-markdown' 5 | import { slugify } from 'transliteration' 6 | import yargs from 'yargs' 7 | import dayjs from 'dayjs' 8 | // import { Post } from 'blog/types' 9 | 10 | const args: any = yargs.option('production', { 11 | boolean: true, 12 | default: false, 13 | alias: 'p', 14 | }).argv 15 | 16 | // 读取 posts 目录下文件及文件夹列表 17 | const list = fs.readdirSync(path.resolve(__dirname, '../posts')) 18 | 19 | // 过滤取出 md 文件列表 20 | const posts = list.filter(item => item.endsWith('.md')).map(item => item.replace('.md', '')) 21 | 22 | // 读取 md 文章 frontmatter 数组并组合 23 | const jsonData: Array = posts 24 | .map(post => { 25 | const { data, content } = matter.read(`./posts/${post}.md`) 26 | 27 | const slugifiedFilename = slugify(data.title, { 28 | trim: true, 29 | replace: { 30 | '——': '-', 31 | }, 32 | }) 33 | 34 | return { 35 | filename: post, 36 | slugifiedFilename, 37 | ...data, 38 | description: summarize(content).substr(0, 60), 39 | } as any 40 | }) 41 | .filter(item => { 42 | if (args.production) { 43 | // 过滤博客草稿草稿 44 | return !item!.draft 45 | } 46 | 47 | return true 48 | }) 49 | 50 | // 写入 json 51 | fs.writeJson('./posts/posts.json', jsonData, { 52 | spaces: 2, 53 | }) 54 | 55 | // metaData 56 | const metaData = { 57 | updatedAt: dayjs().format('YYYY-MM-DD HH:mm:ss'), 58 | } 59 | 60 | fs.writeJson('./posts/meta.json', metaData, { 61 | spaces: 2, 62 | }) 63 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'plugin:vue/recommended', 9 | 'plugin:nuxt/recommended', 10 | 'standard', 11 | ], 12 | plugins: ['vue', '@typescript-eslint'], 13 | parserOptions: { 14 | parser: '@typescript-eslint/parser', 15 | ecmaVersion: 2020, 16 | sourceType: 'module', 17 | }, 18 | // add your custom rules here 19 | rules: { 20 | 'no-undef': 'off', 21 | 'import/namespace': 'off', 22 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 23 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 24 | 'no-unreachable': process.env.NODE_ENV === 'production' ? 'error' : 'off', 25 | 'comma-dangle': ['error', 'always-multiline'], 26 | 'array-element-newline': [ 27 | 'error', 28 | { 29 | multiline: true, 30 | minItems: 3, 31 | }, 32 | ], 33 | 'array-bracket-newline': [ 34 | 'error', 35 | { 36 | multiline: true, 37 | minItems: 3, 38 | }, 39 | ], 40 | // disable the rule for all files 41 | '@typescript-eslint/explicit-function-return-type': 'off', 42 | '@typescript-eslint/explicit-module-boundary-types': 'off', 43 | 44 | }, 45 | overrides: [ 46 | { 47 | // enable the rule specifically for TypeScript files 48 | files: ['*.ts', '*.tsx'], 49 | rules: { 50 | '@typescript-eslint/explicit-function-return-type': ['error'], 51 | '@typescript-eslint/explicit-module-boundary-types': 'error', 52 | }, 53 | }, 54 | ], 55 | } 56 | -------------------------------------------------------------------------------- /pages/resume/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 个人简历 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 58 | 59 | 66 | -------------------------------------------------------------------------------- /pages/resume/components/BasicInfo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | {{ item.label }} 10 | {{ item.value }} 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 62 | 63 | 83 | -------------------------------------------------------------------------------- /assets/sprite/svg/edit.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 7 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './app.html', 4 | './components/**/*.{js,ts,vue}', 5 | './layouts/**/*.vue', 6 | './pages/**/*.vue', 7 | './plugins/**/*.{js,ts}', 8 | './nuxt.config.{js,ts}', 9 | ], 10 | 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: '1rem', 15 | }, 16 | extend: { 17 | spacing: { 18 | 128: '32rem', 19 | 144: '36rem', 20 | }, 21 | typography: { 22 | DEFAULT: { 23 | css: { 24 | maxWidth: '1400px', 25 | color: '#2b2b2b', 26 | strong: { 27 | fontWeight: '800', 28 | }, 29 | p: { 30 | marginTop: 0, 31 | marginBottom: '1.25rem', 32 | textAlign: 'justify', 33 | hyphens: 'auto', 34 | }, 35 | h1: { 36 | fontSize: '24px', 37 | fontWeight: '600', 38 | }, 39 | h2: { 40 | fontSize: '22px', 41 | margin: '1rem 0', 42 | }, 43 | h3: { 44 | fontSize: '20px', 45 | margin: '0.5rem 0', 46 | }, 47 | h4: { 48 | fontSize: '18px', 49 | }, 50 | a: { 51 | }, 52 | figure: { 53 | margin: '1rem 0', 54 | }, 55 | img: { 56 | margin: '1.25rem auto', 57 | }, 58 | figcaption: { 59 | textAlign: 'center', 60 | fontSize: '0.875rem', 61 | color: '#1c1c1c', 62 | }, 63 | // ... 64 | }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | plugins: [ 70 | require('@tailwindcss/typography'), 71 | require('@tailwindcss/line-clamp'), 72 | require('@tailwindcss/aspect-ratio'), 73 | ], 74 | } 75 | -------------------------------------------------------------------------------- /posts/使用-Laravel-数据填充功能生成中文测试数据.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 使用 Laravel 数据填充功能生成中文测试数据 3 | date: 2019-03-10 16:38:18 4 | top_img: ./top_img.jpg 5 | tags: 6 | - laravel 7 | - 数据填充 8 | categories: 9 | - php 10 | - Laravel 11 | --- 12 | 13 | 今晚……不对,是昨晚,折腾一个的小项目,发现自动填充的中文数据显示起来总不太美观,于是开始琢磨如何填充中文数据进行测试。 14 | 15 | 然而一番搜索后惊奇的发现,官方、以及一些非官方的文档均未提及这一功能。期间看到一篇他人的“经验”文章,虽然可以实现这一需求,却要求修改 vendor 目录下 fzaninotto/Faker 包的源码,对于一个中了 Laravel 的“优雅之毒”的人来说,怎能容忍如此风骚的操作? 16 | 17 | 一定有更好的办法…… 18 | 19 | 继续理清 Laravel 模型工厂原理之后,终于有所进展。发现其实只需要一个小小的修改就可以实现这一功能。 20 | 21 | - 根据官方示例的模型工厂代码 22 | 23 | ```php 24 | $factory->define(App\Product::class, function (Faker\Generator $faker) { 25 | return [ 26 | 'user_id' => 1, 27 | 'name' => $faker->name, 28 | 'mobile' => $faker->phoneNumber, 29 | 'province' => $faker->state, 30 | 'city' => $faker->city, 31 | 'area' => $faker->area, 32 | 'address' => $faker->streetAddress, 33 | 'postcode' => $faker->postcode, 34 | ]; 35 | }); 36 | ``` 37 | 38 | - 调整后的代码 39 | 40 | ```php 41 | $factory->define(App\Address::class, function () { 42 | $faker = Faker\Factory::create('zh_CN'); 43 | 44 | return [ 45 | 'user_id' => 1, 46 | 'name' => $faker->name, 47 | 'mobile' => $faker->phoneNumber, 48 | 'province' => $faker->state, 49 | 'city' => $faker->city, 50 | 'area' => $faker->area, 51 | 'address' => $faker->streetAddress, 52 | 'postcode' => $faker->postcode, 53 | ]; 54 | }); 55 | ``` 56 | 57 | 调整前,使用依赖注入的 `Faker\Generator` 是使用的默认语言,即英文。 58 | 59 | 调整后, `Faker\Factory::create('zh_CN')` 也会返回一个 `Faker\Generator`, 但它是使用汉语初始化的。 60 | 61 | **事实上 Faker 本地化对于中文的支持仍有部分待完善,使用暂时不支持生成随机中文句子或者段落(相应的方法返回的仍然会是英文的),但我相信不久之后会有大牛实现这一些功能。** 62 | 63 | 最后,上图,实际生成数据效果如下: 64 |  65 | > 请别纠结省市区从属关系,数据仅供测试而已 :smile: 66 | 67 | **评论中大牛提醒后发现, Laravel5.4 及更新版本其实已经考虑了这一问题,并设置了相关的配置项 `app.faker_locale`,只不过在文档和默认的配置文件中看不到这一参数。相关源码在 `Illuminate\Database\DatabaseServiceProvider` 类中,可以查看源码来判断是否支持这一配置项。对于支持的版本,只需要在 `config\app.php` 文件中加入 `faker_locale => 'zh_CN'` 就可以实现了** 68 | -------------------------------------------------------------------------------- /posts/intervention-image-中的一个小坑及其破解之法.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: intervention/image 中的一个小坑及其破解之法 3 | date: 2019-03-10 16:39:54 4 | top_img: ./top_img.jpg 5 | tags: 6 | - intervention-image 7 | - php 8 | categories: php 9 | --- 10 | 11 | 事实上 intervention/iamge 用了很有些时日了,它的 api 设计得很简洁,文档也很全面,用起来相当顺手。 12 | 13 | 不过最近无意间发现了一个小坑。因为需要合成带微信头像的二维码,我使用 `Image::make($avatarUrl)` (这里的 $avatarUrl 是微信头像的链接)来产生头像,然后合成到二维码图像中去(还包括一些其它操作,比如使用模板背景、写入文字)。 14 | 15 | 写完之后一运行,发现相当慢,平均耗时 23 秒左右。起初以为是因为合成过程中进行的操作比较多、尺寸比较大,本来就应该是这个速度。不过后来闲下来,开始试着优化,即使不能提升速度,至少也搞清楚到底是什么原因这么耗时。 16 | 17 | 这一通折腾下来,发现真相竟然与合成操作的多少、尺寸没有多大关系。而关键在于我创建头像数据的姿势。 18 | 19 | 为了说明这个问题,特意写了下面的代码进行对比。 20 | 21 | ```php 22 | // 记录开始时间 23 | $startTimestamp = microtime(true); 24 | 25 | $url = 'http://wx.qlogo.cn/mmopen/XxT9TiaJ1ibf06TNRCMjQADS4opDHvQLguLZHpqkRlvuJYZicvJW4iaOalPsKIs0kpZ3F6864ZzibyObYiaucUQSrdp4pFTNDyIpxw/0'; 26 | 27 | $avatar = \Image::make($url); 28 | 29 | // 记录结束时间 30 | $endTimestamp = microtime(true); 31 | 32 | info($startTimestamp); 33 | info($endTimestamp); 34 | info($endTimestamp - $startTimestamp); 35 | ``` 36 | 37 |  38 | 39 | 上面这段代码使用 `Image::make($url)` 的形式,直接从 url 生成头像。从记录的日志数据来看,耗时基本上在 16 秒左右。 40 | 41 | 后来,想到了一个新姿势,其实也就是在尝试优化的过程中折腾时想到的。见下面代码: 42 | 43 | ```php 44 | $startTimestamp = microtime(true); 45 | 46 | $client = new \GuzzleHttp\Client(); 47 | 48 | $url = 'http://wx.qlogo.cn/mmopen/XxT9TiaJ1ibf06TNRCMjQADS4opDHvQLguLZHpqkRlvuJYZicvJW4iaOalPsKIs0kpZ3F6864ZzibyObYiaucUQSrdp4pFTNDyIpxw/0'; 49 | 50 | $avatarResponse = $client->get($url); 51 | 52 | $avatar = \Image::make($avatarResponse->getBody()->getContents()); 53 | 54 | $endTimestamp = microtime(true); 55 | 56 | info($startTimestamp); 57 | info($endTimestamp); 58 | info($endTimestamp - $startTimestamp); 59 | ``` 60 | 61 | 在这里我先使用 GuzzleHttp 获取头像,再使用 Image::make($data) 创建头像。 62 | 63 | 注意,要高潮了…… :sunglasses: 64 | 65 | 看看下面的日志截图,三次平均耗时在 0.07 秒左右,和前面的 16 秒相比,差了 200 多倍。 66 |  67 | 68 | 至于为什么会出现这种现象,自己也没搞清楚,但这无疑是一点比较有用且小众的经验。 69 | -------------------------------------------------------------------------------- /content/posts/使用-Laravel-数据填充功能生成中文测试数据.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 使用 Laravel 数据填充功能生成中文测试数据 3 | date: 2019-03-10 16:38:18 4 | top_img: /top_img.jpg 5 | tags: 6 | - laravel 7 | - 数据填充 8 | categories: 9 | - php 10 | - Laravel 11 | draft: false 12 | --- 13 | 14 | 今晚……不对,是昨晚,折腾一个的小项目,发现自动填充的中文数据显示起来总不太美观,于是开始琢磨如何填充中文数据进行测试。 15 | 16 | 然而一番搜索后惊奇的发现,官方、以及一些非官方的文档均未提及这一功能。期间看到一篇他人的“经验”文章,虽然可以实现这一需求,却要求修改 vendor 目录下 fzaninotto/Faker 包的源码,对于一个中了 Laravel 的“优雅之毒”的人来说,怎能容忍如此风骚的操作? 17 | 18 | 一定有更好的办法…… 19 | 20 | 继续理清 Laravel 模型工厂原理之后,终于有所进展。发现其实只需要一个小小的修改就可以实现这一功能。 21 | 22 | - 根据官方示例的模型工厂代码 23 | 24 | ```php 25 | $factory->define(App\Product::class, function (Faker\Generator $faker) { 26 | return [ 27 | 'user_id' => 1, 28 | 'name' => $faker->name, 29 | 'mobile' => $faker->phoneNumber, 30 | 'province' => $faker->state, 31 | 'city' => $faker->city, 32 | 'area' => $faker->area, 33 | 'address' => $faker->streetAddress, 34 | 'postcode' => $faker->postcode, 35 | ]; 36 | }); 37 | ``` 38 | 39 | - 调整后的代码 40 | 41 | ```php 42 | $factory->define(App\Address::class, function () { 43 | $faker = Faker\Factory::create('zh_CN'); 44 | 45 | return [ 46 | 'user_id' => 1, 47 | 'name' => $faker->name, 48 | 'mobile' => $faker->phoneNumber, 49 | 'province' => $faker->state, 50 | 'city' => $faker->city, 51 | 'area' => $faker->area, 52 | 'address' => $faker->streetAddress, 53 | 'postcode' => $faker->postcode, 54 | ]; 55 | }); 56 | ``` 57 | 58 | 调整前,使用依赖注入的 `Faker\Generator` 是使用的默认语言,即英文。 59 | 60 | 调整后, `Faker\Factory::create('zh_CN')` 也会返回一个 `Faker\Generator`, 但它是使用汉语初始化的。 61 | 62 | **事实上 Faker 本地化对于中文的支持仍有部分待完善,使用暂时不支持生成随机中文句子或者段落(相应的方法返回的仍然会是英文的),但我相信不久之后会有大牛实现这一些功能。** 63 | 64 | 最后,上图,实际生成数据效果如下: 65 |  66 | > 请别纠结省市区从属关系,数据仅供测试而已 :smile: 67 | 68 | **评论中大牛提醒后发现, Laravel5.4 及更新版本其实已经考虑了这一问题,并设置了相关的配置项 `app.faker_locale`,只不过在文档和默认的配置文件中看不到这一参数。相关源码在 `Illuminate\Database\DatabaseServiceProvider` 类中,可以查看源码来判断是否支持这一配置项。对于支持的版本,只需要在 `config\app.php` 文件中加入 `faker_locale => 'zh_CN'` 就可以实现了** 69 | -------------------------------------------------------------------------------- /content/posts/intervention-image-中的一个小坑及其破解之法.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: intervention/image 中的一个小坑及其破解之法 3 | date: 2019-03-10 16:39:54 4 | top_img: /top_img.jpg 5 | tags: 6 | - intervention-image 7 | - php 8 | categories: php 9 | draft: false 10 | --- 11 | 12 | 事实上 intervention/iamge 用了很有些时日了,它的 api 设计得很简洁,文档也很全面,用起来相当顺手。 13 | 14 | 不过最近无意间发现了一个小坑。因为需要合成带微信头像的二维码,我使用 `Image::make($avatarUrl)` (这里的 $avatarUrl 是微信头像的链接)来产生头像,然后合成到二维码图像中去(还包括一些其它操作,比如使用模板背景、写入文字)。 15 | 16 | 写完之后一运行,发现相当慢,平均耗时 23 秒左右。起初以为是因为合成过程中进行的操作比较多、尺寸比较大,本来就应该是这个速度。不过后来闲下来,开始试着优化,即使不能提升速度,至少也搞清楚到底是什么原因这么耗时。 17 | 18 | 这一通折腾下来,发现真相竟然与合成操作的多少、尺寸没有多大关系。而关键在于我创建头像数据的姿势。 19 | 20 | 为了说明这个问题,特意写了下面的代码进行对比。 21 | 22 | ```php 23 | // 记录开始时间 24 | $startTimestamp = microtime(true); 25 | 26 | $url = 'http://wx.qlogo.cn/mmopen/XxT9TiaJ1ibf06TNRCMjQADS4opDHvQLguLZHpqkRlvuJYZicvJW4iaOalPsKIs0kpZ3F6864ZzibyObYiaucUQSrdp4pFTNDyIpxw/0'; 27 | 28 | $avatar = \Image::make($url); 29 | 30 | // 记录结束时间 31 | $endTimestamp = microtime(true); 32 | 33 | info($startTimestamp); 34 | info($endTimestamp); 35 | info($endTimestamp - $startTimestamp); 36 | ``` 37 | 38 |  39 | 40 | 上面这段代码使用 `Image::make($url)` 的形式,直接从 url 生成头像。从记录的日志数据来看,耗时基本上在 16 秒左右。 41 | 42 | 后来,想到了一个新姿势,其实也就是在尝试优化的过程中折腾时想到的。见下面代码: 43 | 44 | ```php 45 | $startTimestamp = microtime(true); 46 | 47 | $client = new \GuzzleHttp\Client(); 48 | 49 | $url = 'http://wx.qlogo.cn/mmopen/XxT9TiaJ1ibf06TNRCMjQADS4opDHvQLguLZHpqkRlvuJYZicvJW4iaOalPsKIs0kpZ3F6864ZzibyObYiaucUQSrdp4pFTNDyIpxw/0'; 50 | 51 | $avatarResponse = $client->get($url); 52 | 53 | $avatar = \Image::make($avatarResponse->getBody()->getContents()); 54 | 55 | $endTimestamp = microtime(true); 56 | 57 | info($startTimestamp); 58 | info($endTimestamp); 59 | info($endTimestamp - $startTimestamp); 60 | ``` 61 | 62 | 在这里我先使用 GuzzleHttp 获取头像,再使用 Image::make($data) 创建头像。 63 | 64 | 注意,要高潮了…… :sunglasses: 65 | 66 | 看看下面的日志截图,三次平均耗时在 0.07 秒左右,和前面的 16 秒相比,差了 200 多倍。 67 |  68 | 69 | 至于为什么会出现这种现象,自己也没搞清楚,但这无疑是一点比较有用且小众的经验。 70 | -------------------------------------------------------------------------------- /assets/sprite/svg/resume/blog.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | -------------------------------------------------------------------------------- /assets/sprite/svg/cup.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/sprite/svg/chart.svg: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /pages/resume/components/OpenSource.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 开源经历 5 | 6 | 7 | 11 | 12 | 17 | 18 | 24 | 25 | 26 | {{ repo.languages.nodes[0].name }} 31 | 32 | 33 | 34 | {{ repo.stargazers.totalCount }} 35 | 36 | 37 | 38 | 39 | 40 | {{ repo.forks.totalCount }} 41 | 42 | 43 | 44 | 45 | {{ repo.description }} 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 80 | 81 | 98 | -------------------------------------------------------------------------------- /pages/resume/components/WorkExperience.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 工作经历 5 | 6 | 7 | 8 | 13 | 14 | {{ item.duration }} 15 | 16 | 17 | {{ item.company }} 18 | {{ item.title }} 19 | 20 | {{ item.description }} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 70 | 71 | 109 | -------------------------------------------------------------------------------- /posts/highlight-js-在-Vue-中使用的一点儿经验.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: highlight.js 在 Vue 中使用的一点儿经验 3 | date: 2019-03-10 16:42:07 4 | top_img: ./top_img.jpg 5 | tags: 6 | - vue 7 | - highlight.js 8 | categories: 9 | - js 10 | - vue 11 | --- 12 | 13 | 使用 markdown 来给程序写文档是非常方便的,自从用顺了 markdown 之后,都很久没打开过 Word 了。 14 | 15 | 既然是程序的文档,少不了需要插入一些示例代码,而对代码进行语法高亮渲染并配以合适的颜色主题,会让文档显得更炫,也更便于阅读。 16 | 17 | 要实现文档代码高亮渲染其实并不难。 18 | 19 | ### 实现方法 20 | 21 | 首先,把 markdown 文件加载为 vue 组件,这需要一个合适的 loader,自己目前使用 `vue-markdown-loader`。webpack 配置的 `module.rules` 中进行如下配置: 22 | 23 | ```js 24 | { 25 | test: /\.md$/, 26 | loader: 'vue-markdown-loader', 27 | options: { 28 | preset: 'default', 29 | breaks: true, 30 | preventExtract: true 31 | } 32 | } 33 | ``` 34 | 35 | 然后就可以在项目中直接 import md 文件了。比如: 36 | 37 | ```html 38 | 39 | 40 | 41 | 42 | 49 | ``` 50 | 51 | 当然,通常情况下,我们会与 vue-router 一起使用,把 md 文件作为一个视图组件加载到 `router-view` 中去。 52 | 53 | ```js 54 | { 55 | path: 'path/home', 56 | component: () => import('../markdown/home.md') 57 | }, 58 | ``` 59 | 60 | 看到这里可能奇怪,这些与文题中提到的 highlight.js 有毛关系?这是因为,vue-markdown-loader 中已经内置了对代码高这的支持。你只需要在页面中引入相关的样式,例如: 61 | 62 | ```js 63 | import 'highlight.js/styles/atom-one-dark.css' 64 | ``` 65 | 66 | 然后主可以看到代码高亮的效果,通常是这样的。 67 | 68 |  69 | 70 | 71 | 看起来还不错,但这样的高亮有个问题,那就是他的背景色并不随着你所加载了 highlight.js 主题样式而改变,而且不同语言的代码在配色上的一些差异也没有很好的渲染出来。而从 highlight.js 官网示例可以看到,这些问题本不应该出现的。 72 | 73 | 为了实现与 highlight.js 官网示例中的主题效果,可以在页面中自己完成代码高亮的渲染。 74 | 75 | ```js 76 | 98 | ``` 99 | 100 | 可以看到,代码中使用了 highlight.js 的 `highlightBlock()` 方法而不是官方默认示例里提到的 `initHighlighting()`,因为后者一般用于**静态页面**的渲染。如果使用它,当使用 vue-router 导航到一个新的‘页面’之后,新页面中的代码块可能无法被正确渲染。这也是为什么在 updated 钩子中再次调用 `highlightCode()`的原因。(实际上自己在此坑了很久,查阅不少文档才找到这一原因) 101 | 102 | 做完这些之后再看渲染效果: 103 | 104 |  105 | 106 | 果然好多了! 107 | 108 | ### 后记 109 | 110 | 既然是自己渲染代码高亮,那么其实 loader 中对代码块块的处理就不必要或者显得有点儿多余了,因为这些处理会增加一些计算量。所以你也可以找一些别的 loader 来替代 vue-markdown-loader,甚至尝试自己写一个 loader。 111 | 112 | 对于一个软件,官方文档是有必要仔细读的,就像前面提到的 highlight.js 中 `initHighlighting()` 方法的问题,其实在官方文档中也有解释。 113 | -------------------------------------------------------------------------------- /content/posts/highlight-js-在-Vue-中使用的一点儿经验.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: highlight.js 在 Vue 中使用的一点儿经验 3 | date: 2019-03-10 16:42:07 4 | top_img: /top_img.jpg 5 | tags: 6 | - vue 7 | - highlight.js 8 | categories: 9 | - js 10 | - vue 11 | draft: false 12 | --- 13 | 14 | 使用 markdown 来给程序写文档是非常方便的,自从用顺了 markdown 之后,都很久没打开过 Word 了。 15 | 16 | 既然是程序的文档,少不了需要插入一些示例代码,而对代码进行语法高亮渲染并配以合适的颜色主题,会让文档显得更炫,也更便于阅读。 17 | 18 | 要实现文档代码高亮渲染其实并不难。 19 | 20 | ### 实现方法 21 | 22 | 首先,把 markdown 文件加载为 vue 组件,这需要一个合适的 loader,自己目前使用 `vue-markdown-loader`。webpack 配置的 `module.rules` 中进行如下配置: 23 | 24 | ```js 25 | { 26 | test: /\.md$/, 27 | loader: 'vue-markdown-loader', 28 | options: { 29 | preset: 'default', 30 | breaks: true, 31 | preventExtract: true 32 | } 33 | } 34 | ``` 35 | 36 | 然后就可以在项目中直接 import md 文件了。比如: 37 | 38 | ```html 39 | 40 | 41 | 42 | 43 | 50 | ``` 51 | 52 | 当然,通常情况下,我们会与 vue-router 一起使用,把 md 文件作为一个视图组件加载到 `router-view` 中去。 53 | 54 | ```js 55 | { 56 | path: 'path/home', 57 | component: () => import('../markdown/home.md') 58 | }, 59 | ``` 60 | 61 | 看到这里可能奇怪,这些与文题中提到的 highlight.js 有毛关系?这是因为,vue-markdown-loader 中已经内置了对代码高这的支持。你只需要在页面中引入相关的样式,例如: 62 | 63 | ```js 64 | import 'highlight.js/styles/atom-one-dark.css' 65 | ``` 66 | 67 | 然后主可以看到代码高亮的效果,通常是这样的。 68 | 69 |  70 | 71 | 72 | 看起来还不错,但这样的高亮有个问题,那就是他的背景色并不随着你所加载了 highlight.js 主题样式而改变,而且不同语言的代码在配色上的一些差异也没有很好的渲染出来。而从 highlight.js 官网示例可以看到,这些问题本不应该出现的。 73 | 74 | 为了实现与 highlight.js 官网示例中的主题效果,可以在页面中自己完成代码高亮的渲染。 75 | 76 | ```js 77 | 99 | ``` 100 | 101 | 可以看到,代码中使用了 highlight.js 的 `highlightBlock()` 方法而不是官方默认示例里提到的 `initHighlighting()`,因为后者一般用于**静态页面**的渲染。如果使用它,当使用 vue-router 导航到一个新的‘页面’之后,新页面中的代码块可能无法被正确渲染。这也是为什么在 updated 钩子中再次调用 `highlightCode()`的原因。(实际上自己在此坑了很久,查阅不少文档才找到这一原因) 102 | 103 | 做完这些之后再看渲染效果: 104 | 105 |  106 | 107 | 果然好多了! 108 | 109 | ### 后记 110 | 111 | 既然是自己渲染代码高亮,那么其实 loader 中对代码块块的处理就不必要或者显得有点儿多余了,因为这些处理会增加一些计算量。所以你也可以找一些别的 loader 来替代 vue-markdown-loader,甚至尝试自己写一个 loader。 112 | 113 | 对于一个软件,官方文档是有必要仔细读的,就像前面提到的 highlight.js 中 `initHighlighting()` 方法的问题,其实在官方文档中也有解释。 114 | -------------------------------------------------------------------------------- /posts/swiper-js-loop-小坑.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: swiper.js loop 小坑 3 | date: '2020-02-08 02:15:49' 4 | top_img: ./top_img.png 5 | tags: 6 | - 'swiper.js' 7 | - 'vue' 8 | categories: 9 | - '前端' 10 | --- 11 | 12 | swiper.js 是一款强大的插件,也是我最喜欢的开源插件之一。它可以轻松实现轮播、tab 标签以及各种风骚的页面滑动效果。 13 | 14 | 我在自己的很多项目里都用到了 swiper,配合 vue 使用真的非常方便。不过近日遇到一个小坑,几番搜索和试验,才发现与 loop 特性有关。 15 | 16 | 具体现象是,当 swiper 开启 loop 属性实现循环轮播,同时用 vue 的事件绑定语法为每个轮播页绑定事件,当轮播到特定的页面时绑定的事件无法被正常监听。 17 | 18 | 例如下面的代码: 19 | 20 | ```html 21 | 22 | 23 | 24 | 25 | Slide {{ i }} 30 | 31 | 32 | 33 | 34 | 35 | 57 | ``` 58 | 59 | 这段代码实际上添加了两页轮番,loop = true 会实现循环轮播的效果。但实际使用过程中发现,Slide1 上绑定的 click 事件监听,除了初始时第一次显示时能正常捕获外,当连续向左滚动之后显示的 Slide1 页上点击就没效果了。同样,向右滑动时,Slide2 上绑定的事件监听器也可能出现问题。 60 | 61 |  62 | 63 | 打开调试控制台查看页面元素,发现除了实际添加的两页轮播对应的 slide 元素之外,左边多了一个复制的 Slide2 元素,而右边则多了一个复制的 Slide1。 64 | 65 |  66 | 67 | 原来 Swiper 是通过在实际轮播页前后复制若干个页面来实现 loop(首尾相连循环滚动)效果的,联想到前面提到的问题现象,马上猜测是因为虽然复制了轮播页元素但 vue 所绑定的事件处理器却没有被复制。于是进一步查看了这个元素上绑定的事件监听器,果然如此。 68 | 69 | 找到的问题原因,那也就有头绪了。Swiper.js 自己其实也提供了一套事件绑定机制,我们只需要把原代码里 vue 指定绑定的事件监听器通过 Swiper 初始选项中绑定就好了。调整后的代码如下。 70 | 71 | ```vue 72 | 73 | 74 | 75 | 76 | Slide {{ i }} 82 | 83 | 84 | 85 | 86 | 87 | 109 | ``` 110 | 111 | 再次试用点击事件,一切正常了。 112 | 113 |  114 | -------------------------------------------------------------------------------- /content/posts/swiper-js-loop-小坑.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: swiper.js loop 小坑 3 | date: '2020-02-08 02:15:49' 4 | top_img: /top_img.png 5 | tags: 6 | - 'swiper.js' 7 | - 'vue' 8 | categories: 9 | - '前端' 10 | draft: false 11 | --- 12 | 13 | swiper.js 是一款强大的插件,也是我最喜欢的开源插件之一。它可以轻松实现轮播、tab 标签以及各种风骚的页面滑动效果。 14 | 15 | 我在自己的很多项目里都用到了 swiper,配合 vue 使用真的非常方便。不过近日遇到一个小坑,几番搜索和试验,才发现与 loop 特性有关。 16 | 17 | 具体现象是,当 swiper 开启 loop 属性实现循环轮播,同时用 vue 的事件绑定语法为每个轮播页绑定事件,当轮播到特定的页面时绑定的事件无法被正常监听。 18 | 19 | 例如下面的代码: 20 | 21 | ```html 22 | 23 | 24 | 25 | 26 | Slide {{ i }} 31 | 32 | 33 | 34 | 35 | 36 | 58 | ``` 59 | 60 | 这段代码实际上添加了两页轮番,loop = true 会实现循环轮播的效果。但实际使用过程中发现,Slide1 上绑定的 click 事件监听,除了初始时第一次显示时能正常捕获外,当连续向左滚动之后显示的 Slide1 页上点击就没效果了。同样,向右滑动时,Slide2 上绑定的事件监听器也可能出现问题。 61 | 62 |  63 | 64 | 打开调试控制台查看页面元素,发现除了实际添加的两页轮播对应的 slide 元素之外,左边多了一个复制的 Slide2 元素,而右边则多了一个复制的 Slide1。 65 | 66 |  67 | 68 | 原来 Swiper 是通过在实际轮播页前后复制若干个页面来实现 loop(首尾相连循环滚动)效果的,联想到前面提到的问题现象,马上猜测是因为虽然复制了轮播页元素但 vue 所绑定的事件处理器却没有被复制。于是进一步查看了这个元素上绑定的事件监听器,果然如此。 69 | 70 | 找到的问题原因,那也就有头绪了。Swiper.js 自己其实也提供了一套事件绑定机制,我们只需要把原代码里 vue 指定绑定的事件监听器通过 Swiper 初始选项中绑定就好了。调整后的代码如下。 71 | 72 | ```vue 73 | 74 | 75 | 76 | 77 | Slide {{ i }} 83 | 84 | 85 | 86 | 87 | 88 | 110 | ``` 111 | 112 | 再次试用点击事件,一切正常了。 113 | 114 |  115 | -------------------------------------------------------------------------------- /pages/resume/components/Skills.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 语言/框架技能栈 5 | 6 | 7 | 12 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 开发工具技能栈 27 | 28 | 29 | 30 | 35 | 39 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 133 | 134 | 147 | -------------------------------------------------------------------------------- /pages/open-source.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 16 | {{ repo.description }} 17 | 18 | 19 | 20 | {{ repo.primaryLanguage.name }} 25 | 28 | {{ repo.stargazerCount }} 29 | 30 | 31 | {{ repo.forkCount }} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 106 | -------------------------------------------------------------------------------- /assets/scss/app.scss: -------------------------------------------------------------------------------- 1 | @import "normalize.css/normalize.css"; 2 | @import "heti/lib/heti.scss"; 3 | 4 | /* Lite version */ 5 | @import 'lxgw-wenkai-lite-webfont/style.css'; 6 | 7 | @tailwind base; 8 | @tailwind components; 9 | @tailwind utilities; 10 | 11 | body { 12 | background-color: #e8e8e8; 13 | /* 霞鹜文楷 */ 14 | font-family: "LXGW WenKai Lite", sans-serif; 15 | } 16 | 17 | * { 18 | box-sizing: border-box; 19 | } 20 | 21 | /* 霞鹜文楷 Mono */ 22 | pre, code { 23 | font-family: "LXGW WenKai Mono Lite", sans-serif 24 | } 25 | 26 | .heti { 27 | max-width: none !important; 28 | /* 霞鹜文楷 */ 29 | font-family: "LXGW WenKai Lite", sans-serif; 30 | 31 | /* 霞鹜文楷 Mono */ 32 | pre, code { 33 | font-family: "LXGW WenKai Mono Lite", sans-serif 34 | } 35 | } 36 | 37 | /// Mixin to customize scrollbars 38 | /// Beware, this does not work in all browsers 39 | /// @author Hugo Giraudel 40 | /// @param {Length} $size - Horizontal scrollbar's height and vertical scrollbar's width 41 | /// @param {Color} $foreground-color - Scrollbar's color 42 | /// @param {Color} $background-color [mix($foreground-color, white, 50%)] - Scrollbar's color 43 | /// @example scss - Scrollbar styling 44 | /// @include scrollbars(.5em, slategray); 45 | @mixin scrollbars($size, $foreground-color, $background-color: mix($foreground-color, white, 50%)) { 46 | // For Google Chrome 47 | ::-webkit-scrollbar { 48 | width: $size; 49 | height: $size; 50 | } 51 | 52 | ::-webkit-scrollbar-thumb { 53 | background: $foreground-color; 54 | border-radius: $size; 55 | } 56 | 57 | ::-webkit-scrollbar-track { 58 | background: $background-color; 59 | } 60 | 61 | // For Internet Explorer 62 | iframe, 63 | body { 64 | scrollbar-face-color: $foreground-color; 65 | scrollbar-track-color: $background-color; 66 | } 67 | } 68 | 69 | @include scrollbars(0.5em, slategray); 70 | 71 | .custom-block .custom-block-title { 72 | font-weight: 600; 73 | margin-bottom: .4rem 74 | } 75 | 76 | .custom-block.danger, 77 | .custom-block.tip, 78 | .custom-block.warning { 79 | padding: .1rem 1.5rem; 80 | border-left-width: .5rem; 81 | border-left-style: solid; 82 | margin: 1rem 0 83 | } 84 | 85 | .custom-block.tip { 86 | background-color: #f3f5f7; 87 | border-color: #42b983 88 | } 89 | 90 | .custom-block.warning { 91 | background-color: rgba(255,229,100,.3); 92 | border-color: #e7c000; 93 | color: #6b5900 94 | } 95 | 96 | .custom-block.warning .custom-block-title { 97 | color: #b29400 98 | } 99 | 100 | .custom-block.warning a { 101 | color: #2c3e50 102 | } 103 | 104 | .custom-block.danger { 105 | background-color: #ffe6e6; 106 | border-color: #c00; 107 | color: #4d0000 108 | } 109 | 110 | .custom-block.danger .custom-block-title { 111 | color: #900 112 | } 113 | 114 | .custom-block.danger a { 115 | color: #2c3e50 116 | } 117 | 118 | .grayscale { 119 | // 2020-04-04 页面灰白化 120 | filter: grayscale(100%) !important; 121 | } 122 | -------------------------------------------------------------------------------- /posts/Laravel-自动转换长整型雪花-ID-为字符串.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Laravel 自动转换长整型雪花 ID 为字符串 3 | date: '2020-10-Mo 16:05:59' 4 | top_img: ./top_img.jpg 5 | tags: 6 | - Laravel 7 | - 雪花 ID 8 | categories: 9 | - Laravel 10 | --- 11 | 12 | 在设计 API 时,出于安全性等因素考虑,有时需要放弃使用自增 ID,使 ID 非连续且不可猜测。通常可以使用 Hash id,UUID,雪花 ID 等来实现。 13 | 14 | 在最近的一个项目中,我尝试使用雪花 ID。一通折腾下来发现,逼格挺高,实现也挺简单。然而当我继续撸起袖子与前端部分对接时,却出现了 JS 精度丢失问题,因为存储的 ID 是一个 unsigned bigint 型的值。(至于为什么会有精度丢失现象,这里就不具体解释了,不清楚的可以自行搜索),本文主要介绍解决办法。 15 | 16 | 想要解决这问题,基本原理也很简单,就是把 ID 转成字符串再返回给前端。 17 | 18 | ## 错误尝试 19 | 20 | 一开始我想到的是使用 Laravel Eloquent 模型的模型访问器。只要给需要转换的模型加一个 getIdAttribute,将 ID 转成字符串不就行了嘛? 21 | 22 | 如:App\Models\User 模型里这样写: 23 | 24 | ```php 25 | /** 26 | * @return string 27 | */ 28 | public function getIdAttribute() 29 | { 30 | return strval($this->attributes['id']); 31 | } 32 | ``` 33 | 34 | 但事实并非如此,属性访问器确实能让 API 返回给前端的 ID 变为字符串。但同时也会影响关联模型插入、修改时的结果,例如,user 关联的了 post 模型,使用 $user->posts()->saveMany(...); 这种方式保存的新的 posts 记录,对应的 user_id 会为空。 35 | 36 | 这也不难理解,因为模型访问器是要参与模型相关处理的,访问器将 ID 由数字转为了字符串,自然会导致数据错乱。 37 | 38 | ## 正确姿势 39 | 40 | 冷静下来决定先认真思考再动手,查阅了官方文档,才发现 Resource 正是我想要的。Resource 只会影响返回给前端的数据,我们可以通过自定义 Resource 来实现 API 返回结果的结构、类型转换等功能。转换个 ID 自然也不在话下。 41 | 42 | 为了省事,我直接修改 App\Http\Resource 这个基类。只需要重载它的 toArray() 方法,在其中使用递归,对可能超出 JS 安全数值范围的值进行转换就可以了。大家也可以根据自己的实际情况,新建 Resource 类,如 UserResource 来处理。 43 | 44 | ```php 45 | post,相当于递归处理 71 | if (is_array($parentReturn[$key])) { 72 | $parentReturn[$key] = new Resource($parentReturn[$key]); 73 | } 74 | } 75 | 76 | return $parentReturn; 77 | } 78 | } 79 | ``` 80 | 81 | 然后,在接口控制器中返回 Resource 返回数据,整型字段值就会自动变为字符串了。 82 | 83 | ```php 84 | **也可以全局安全 puppeteer 但就个人经验而言,在项目中安装是比较推荐的做法,因为这样不同项目不会同时受全局安装的 puppeteer 影响,此外项目中安装也方便使用 phpdeployer 进行升级(phpdeploy 升级时不会影响线上项目运行,要知道升级/安装 puppeteer 可是很费时的,有时候还不能保证一次成功)。** 43 | > 安装 puppeteer 时会下载 Chromium-Browser,鉴于咱特殊国情,很有可能出现无法下载的情况,对此,就请大家各显神通吧…… 44 | 45 | ## 使用 46 | 47 | 以采集今日头条手机版页面文章内容为例。 48 | 49 | ```php 50 | use Spatie\Browsershot\Browsershot; 51 | 52 | public function getBodyHtml() 53 | { 54 | $newsUrl = 'https://m.toutiao.com/i6546884151050502660/'; 55 | 56 | $html = Browsershot::url($newsUrl) 57 | ->windowSize(480, 800) 58 | ->userAgent('Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Mobile Safari/537.36') 59 | ->mobile() 60 | ->touch() 61 | ->bodyHtml(); 62 | 63 | \Log::info($html); 64 | } 65 | ``` 66 | 67 | 运行后可以在日志中看到如下内容(截图中只是其中部分) 68 | 69 |  70 | 71 | 此外,也可以将页面保存为图片或 PDF 文件。 72 | 73 | ```php 74 | use Spatie\Browsershot\Browsershot; 75 | 76 | public function getBodyHtml() 77 | { 78 | $newsUrl = 'https://m.toutiao.com/i6546884151050502660/'; 79 | 80 | Browsershot::url($newsUrl) 81 | ->windowSize(480, 800) 82 | ->userAgent('Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Mobile Safari/537.36') 83 | ->mobile() 84 | ->touch() 85 | ->setDelay(1000) 86 | ->save(public_path('images/toutiao.jpg')); 87 | } 88 | ``` 89 | 90 |  91 | 92 | 图片里那些框与系统字体有关。代码中使用了一个 setDelay() 方法,是为了让内容加载完成后再进行截图,简单粗暴,可能不是最好的解决办法。 93 | 94 | ## 可能出现的问题 95 | 96 | - 系统得支持 Chromium 浏览器,当然现在绝大部分浏览器是支持的,要不然也没法,还是用 PhantomJS 吧。 97 | 98 | - 项目中安装了 puppeteer 后调用时有可能出现权限问题,这就需要对项目下 /node_modules/puppeteer 目录赋予适当的权限。 99 | 100 | ## 总结 101 | 102 | puppeteer 被应用于测试、采集等场景,是一个非常有力的工具。对于轻度的采集任务,是够用的,比如本文这类在 Laravel (php) 里来用采集一些小页面,但如果需要快速采集大量内容,还是 Python 啥的吧。:smile: 103 | -------------------------------------------------------------------------------- /content/posts/Laravel-中使用-puppeteer-采集异步加载的网页内容.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Laravel 中使用 puppeteer 采集异步加载的网页内容 3 | date: 2019-03-10 16:44:31 4 | top_img: /top_img.png 5 | tags: 6 | - laravel 7 | - puppeteer 8 | - 采集 9 | categories: 10 | - php 11 | - Laravel 12 | draft: false 13 | --- 14 | 15 |  16 | 17 | 采集网页内容是一项很常见的需求,比较传统的静态页面,curl 就能搞定。但如果页面中有动态加载的内容,比如有些页面里通过 ajax 加载的文章正文内容,又如果有些页面加载完成后进行了一些额外处理(图片地址替换等等……)而你想采集这些处理过后的内容。那么牛逼闪闪的 curl 也束手无策了。 18 | 19 | 做过类似需求的人可能会说,老铁,上 PhantomJS 啊! 20 | 21 | 没错,这是一个办法,而且在相当长的时间里 PhantomJS 是为数不多的能解决这类需求的工具里的佼佼者。 22 | 23 | 但今天这里要介绍的是一个后来居上的工具 -- puppeteer,它是随着 Chrome Headless 技术兴起而快速发展起来的。而且非常关键的是,puppeteer 由 Chrome 的官方团队开发和维护,可以说相当靠谱了! 24 | 25 | puppeteer 是一个 js 包,要想在 Laravel 中使用,得借助于另一神器`spatie/browsershot`。 26 | 27 | ## 安装 28 | 29 | 1. 安装 [spatie/browsershot](https://github.com/spatie/browsershot) 30 | 31 | browsershot 是一个 composer 包,出自于大神团队 [spatie](https://github.com/spatie) 32 | 33 | ```bash 34 | $ composer require spatie/browsershot 35 | ``` 36 | 37 | 2. 安装 [puppeteer](https://github.com/GoogleChrome/puppeteer) 38 | 39 | ```shell 40 | $ npm i puppeteer --save 41 | ``` 42 | 43 | > **也可以全局安全 puppeteer 但就个人经验而言,在项目中安装是比较推荐的做法,因为这样不同项目不会同时受全局安装的 puppeteer 影响,此外项目中安装也方便使用 phpdeployer 进行升级(phpdeploy 升级时不会影响线上项目运行,要知道升级/安装 puppeteer 可是很费时的,有时候还不能保证一次成功)。** 44 | > 安装 puppeteer 时会下载 Chromium-Browser,鉴于咱特殊国情,很有可能出现无法下载的情况,对此,就请大家各显神通吧…… 45 | 46 | ## 使用 47 | 48 | 以采集今日头条手机版页面文章内容为例。 49 | 50 | ```php 51 | use Spatie\Browsershot\Browsershot; 52 | 53 | public function getBodyHtml() 54 | { 55 | $newsUrl = 'https://m.toutiao.com/i6546884151050502660/'; 56 | 57 | $html = Browsershot::url($newsUrl) 58 | ->windowSize(480, 800) 59 | ->userAgent('Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Mobile Safari/537.36') 60 | ->mobile() 61 | ->touch() 62 | ->bodyHtml(); 63 | 64 | \Log::info($html); 65 | } 66 | ``` 67 | 68 | 运行后可以在日志中看到如下内容(截图中只是其中部分) 69 | 70 |  71 | 72 | 此外,也可以将页面保存为图片或 PDF 文件。 73 | 74 | ```php 75 | use Spatie\Browsershot\Browsershot; 76 | 77 | public function getBodyHtml() 78 | { 79 | $newsUrl = 'https://m.toutiao.com/i6546884151050502660/'; 80 | 81 | Browsershot::url($newsUrl) 82 | ->windowSize(480, 800) 83 | ->userAgent('Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Mobile Safari/537.36') 84 | ->mobile() 85 | ->touch() 86 | ->setDelay(1000) 87 | ->save(public_path('images/toutiao.jpg')); 88 | } 89 | ``` 90 | 91 |  92 | 93 | 图片里那些框与系统字体有关。代码中使用了一个 setDelay() 方法,是为了让内容加载完成后再进行截图,简单粗暴,可能不是最好的解决办法。 94 | 95 | ## 可能出现的问题 96 | 97 | - 系统得支持 Chromium 浏览器,当然现在绝大部分浏览器是支持的,要不然也没法,还是用 PhantomJS 吧。 98 | 99 | - 项目中安装了 puppeteer 后调用时有可能出现权限问题,这就需要对项目下 /node_modules/puppeteer 目录赋予适当的权限。 100 | 101 | ## 总结 102 | 103 | puppeteer 被应用于测试、采集等场景,是一个非常有力的工具。对于轻度的采集任务,是够用的,比如本文这类在 Laravel (php) 里来用采集一些小页面,但如果需要快速采集大量内容,还是 Python 啥的吧。:smile: 104 | -------------------------------------------------------------------------------- /pages/resume/components/Cases.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 主要作品/案例 4 | 5 | 6 | 11 | 12 | 15 | {{ item.type }} 16 | 17 | {{ item.name }} 18 | 19 | {{ item.url }} 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 110 | 111 | 132 | -------------------------------------------------------------------------------- /posts/Homestead-laravel-mix-环境下-hmr-的两种玩法.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Homestead + laravel-mix 环境下 hmr 的两种玩法 3 | date: 2019-04-18 01:33:24 4 | top_img: ./top_img.png 5 | tags: 6 | - laravel-mix 7 | - homestead 8 | - hmr 9 | categories: 10 | - 前端 11 | - webpack 12 | --- 13 | 14 | 我在前几天刚写过的[《让 F5 歇一会儿——laravel-mix 自动刷新之道》](https://tianyong90.com/2019/04/12/rang-f5-xie-yi-hui-er-laravel-mix-zi-dong-shua-xin-zhi-dao/)中介绍了 laravel-mix 实现自动刷新的几种方法,其中就有涉及 hmr(Hot Module Replacement),但里面都是以 Laradock 环境为例。对于 Laravel 官方首推的 Homestead 当然也是可以的,只不过用法上有些差别,于加上 laravel-mix 本身的一些 BUG(在 issue 里搜索 `hmr` 结果就有好几页 :smile:),对于刚接触的人来说可能无从下手。 15 | 16 | 本文介绍两种不同的玩法。 17 | 18 | > 首先假定你已经创建了一个 laravel 项目,进行了相关配置(.env 配置及绑定测试域名,如:laravel.test)并已装好了后端依赖 19 | 20 | ## 玩法一:使用虚拟机中的 Node 环境 21 | 22 | 因为 Homestead 提供的环境里默认包含了前端开发所需要的 Node 环境及相关工具(gulp, npm, yarn 等),所以直接使用它们似乎是很省事的选择。 23 | 24 |  25 | 26 | 1. `vagrant ssh` 连接虚拟机,进入项目目录后安装前端依赖 27 | 28 | ```bash 29 | yarn install 30 | ``` 31 | 32 | 1. 在 webpack.mix.js 中调整相关配置 33 | 34 | - 使用 mix.Webpack() 配置 devServer 35 | 36 | ```js 37 | mix.webpackConfig({ 38 | devServer: { 39 | watchOptions: { 40 | poll: 2000, // 这个值可调整,性能高的时候可以调小,也可以直接设置为 true 41 | ignored: /node_modules/, 42 | }, 43 | }, 44 | }) 45 | ``` 46 | 47 | > 这一配置很关键,因为要是仅使用 devServer 的默认 watch 选项,对于虚拟机环境是无效的([见 webpack 文档](https://webpack.js.org/configuration/watch/#watchoptionspoll)) 48 | 49 | - 调整 hmrOptions 50 | 51 | ```js 52 | mix.options({ 53 | hmrOptions: { 54 | host: 'laravel.test', 55 | port: 8080 56 | } 57 | }) 58 | ``` 59 | 60 | 1. 在**虚拟机**终端中执行`yarn run hot`,然后在浏览器中使用绑定的测试域名(如:laravel.test)访问 61 | 62 | 1. 修改 JS 等,自动编译后浏览器中页面即自动更新 63 | 64 | ## 玩法二:使用宿主机中的 Node 环境 65 | 66 | 当然也可以使用宿主机的 Node 环境,对于开发都来说,这些环境应该也是必须的了。 67 | 68 |  69 | 70 | 1. 从宿主机终端进入项目目录并安装前端依赖 71 | 72 | ```bash 73 | yarn install 74 | ``` 75 | 76 | 1. webpack.mix.js 中使用 webpackConfig 进行配置 77 | 78 | ```js 79 | mix.webpackConfig({ 80 | devServer: { 81 | disableHostCheck: true, 82 | }, 83 | // 其它配置 84 | }) 85 | ``` 86 | 87 | > disableHostCheck: true 是为了避免出现下面这种错误。 88 | 89 |  90 | 91 | > 与玩法一中不一样,不再需要特别在 hmrOptions 中指定 devServer 和 host 和 port,使用默认的就好(事实上也**不能**像前面那样指定,因为会出现 IP/端口 冲突) 92 | 93 | 1. 在宿主机终端中执行`yarn run hot`,然后在浏览器中使用绑定的测试域名(如:laravel.test)访问 94 | 95 | 1. 修改 JS 等,自动编译后浏览器中页面即自动更新 96 | 97 | **laravel-mix 4.0.16 中修复了在 windows 中使用 hmr 的 BUG,开发体验相较以前更好。https://github.com/JeffreyWay/laravel-mix/pull/1995** 98 | 99 | ## 总结 100 | 101 | 两种方法并没有谁好谁坏之分,具体使用哪种方法视具体场景及个人喜好而定。就我个人而言,通常使用第二种,主要原因有二: 102 | 103 | - 一是出于性能/延迟方面的考虑,因为在虚拟机中使用轮询(poll)的方式来监听文件变化,当 poll 设置间隔较大时可能会出现一定延迟,而设置太小轮询太频繁则又可能造成一定的性能压力。所以直接使用宿主机的 Node 环境似乎更为划算。 104 | 105 | - 二是自己使用的 IDE(PhpStorm)运行在宿主机(Windows)中,而 PhpStorm 的一些插件(或服务)如 Eslint、TypeScript、 Prettier 需要使用使用本地安装的一些 npm 包,这样就只能在宿主环境里安装依赖。(虽然可以考虑在宿主机全局安装依赖,但诸如 eslint-config-xxx 之类的项目相关的包也全局安装,必然造成混乱) 106 | 107 | 如同学习很多其它新工具新玩法一样,刚开始折腾 laravel-mix 时总是磕磕绊绊(有不少坑),但一旦掌握了窍门,就能极大的方便日常开发,提高工作效率。博客里记下这些,权当备忘,也算是分享,独乐不如众乐。 108 | -------------------------------------------------------------------------------- /content/posts/Homestead-laravel-mix-环境下-hmr-的两种玩法.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Homestead + laravel-mix 环境下 hmr 的两种玩法 3 | date: 2019-04-18 01:33:24 4 | top_img: /top_img.png 5 | tags: 6 | - laravel-mix 7 | - homestead 8 | - hmr 9 | categories: 10 | - 前端 11 | - webpack 12 | draft: false 13 | --- 14 | 15 | 我在前几天刚写过的[《让 F5 歇一会儿——laravel-mix 自动刷新之道》](https://tianyong90.com/2019/04/12/rang-f5-xie-yi-hui-er-laravel-mix-zi-dong-shua-xin-zhi-dao/)中介绍了 laravel-mix 实现自动刷新的几种方法,其中就有涉及 hmr(Hot Module Replacement),但里面都是以 Laradock 环境为例。对于 Laravel 官方首推的 Homestead 当然也是可以的,只不过用法上有些差别,于加上 laravel-mix 本身的一些 BUG(在 issue 里搜索 `hmr` 结果就有好几页 :smile:),对于刚接触的人来说可能无从下手。 16 | 17 | 本文介绍两种不同的玩法。 18 | 19 | > 首先假定你已经创建了一个 laravel 项目,进行了相关配置(.env 配置及绑定测试域名,如:laravel.test)并已装好了后端依赖 20 | 21 | ## 玩法一:使用虚拟机中的 Node 环境 22 | 23 | 因为 Homestead 提供的环境里默认包含了前端开发所需要的 Node 环境及相关工具(gulp, npm, yarn 等),所以直接使用它们似乎是很省事的选择。 24 | 25 |  26 | 27 | 1. `vagrant ssh` 连接虚拟机,进入项目目录后安装前端依赖 28 | 29 | ```bash 30 | yarn install 31 | ``` 32 | 33 | 1. 在 webpack.mix.js 中调整相关配置 34 | 35 | - 使用 mix.Webpack() 配置 devServer 36 | 37 | ```js 38 | mix.webpackConfig({ 39 | devServer: { 40 | watchOptions: { 41 | poll: 2000, // 这个值可调整,性能高的时候可以调小,也可以直接设置为 true 42 | ignored: /node_modules/, 43 | }, 44 | }, 45 | }) 46 | ``` 47 | 48 | > 这一配置很关键,因为要是仅使用 devServer 的默认 watch 选项,对于虚拟机环境是无效的([见 webpack 文档](https://webpack.js.org/configuration/watch/#watchoptionspoll)) 49 | 50 | - 调整 hmrOptions 51 | 52 | ```js 53 | mix.options({ 54 | hmrOptions: { 55 | host: 'laravel.test', 56 | port: 8080 57 | } 58 | }) 59 | ``` 60 | 61 | 1. 在**虚拟机**终端中执行`yarn run hot`,然后在浏览器中使用绑定的测试域名(如:laravel.test)访问 62 | 63 | 1. 修改 JS 等,自动编译后浏览器中页面即自动更新 64 | 65 | ## 玩法二:使用宿主机中的 Node 环境 66 | 67 | 当然也可以使用宿主机的 Node 环境,对于开发都来说,这些环境应该也是必须的了。 68 | 69 |  70 | 71 | 1. 从宿主机终端进入项目目录并安装前端依赖 72 | 73 | ```bash 74 | yarn install 75 | ``` 76 | 77 | 1. webpack.mix.js 中使用 webpackConfig 进行配置 78 | 79 | ```js 80 | mix.webpackConfig({ 81 | devServer: { 82 | disableHostCheck: true, 83 | }, 84 | // 其它配置 85 | }) 86 | ``` 87 | 88 | > disableHostCheck: true 是为了避免出现下面这种错误。 89 | 90 |  91 | 92 | > 与玩法一中不一样,不再需要特别在 hmrOptions 中指定 devServer 和 host 和 port,使用默认的就好(事实上也**不能**像前面那样指定,因为会出现 IP/端口 冲突) 93 | 94 | 1. 在宿主机终端中执行`yarn run hot`,然后在浏览器中使用绑定的测试域名(如:laravel.test)访问 95 | 96 | 1. 修改 JS 等,自动编译后浏览器中页面即自动更新 97 | 98 | **laravel-mix 4.0.16 中修复了在 windows 中使用 hmr 的 BUG,开发体验相较以前更好。https://github.com/JeffreyWay/laravel-mix/pull/1995** 99 | 100 | ## 总结 101 | 102 | 两种方法并没有谁好谁坏之分,具体使用哪种方法视具体场景及个人喜好而定。就我个人而言,通常使用第二种,主要原因有二: 103 | 104 | - 一是出于性能/延迟方面的考虑,因为在虚拟机中使用轮询(poll)的方式来监听文件变化,当 poll 设置间隔较大时可能会出现一定延迟,而设置太小轮询太频繁则又可能造成一定的性能压力。所以直接使用宿主机的 Node 环境似乎更为划算。 105 | 106 | - 二是自己使用的 IDE(PhpStorm)运行在宿主机(Windows)中,而 PhpStorm 的一些插件(或服务)如 Eslint、TypeScript、 Prettier 需要使用使用本地安装的一些 npm 包,这样就只能在宿主环境里安装依赖。(虽然可以考虑在宿主机全局安装依赖,但诸如 eslint-config-xxx 之类的项目相关的包也全局安装,必然造成混乱) 107 | 108 | 如同学习很多其它新工具新玩法一样,刚开始折腾 laravel-mix 时总是磕磕绊绊(有不少坑),但一旦掌握了窍门,就能极大的方便日常开发,提高工作效率。博客里记下这些,权当备忘,也算是分享,独乐不如众乐。 109 | -------------------------------------------------------------------------------- /posts/用-Algolia-DocSearch-轻松实现文档全站搜索.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 用 Algolia DocSearch 轻松实现文档全站搜索 3 | date: 2019-03-10 16:46:31 4 | top_img: ./top_img.jpg 5 | tags: 6 | - Algolia DocSearch 7 | - 全站搜索 8 | categories: 9 | - js 10 | --- 11 | 12 | 话说,有两件事能让程序员抓狂,一是写文档,二是看别人的代码发现没写文档…… 13 | 14 | 没错,咱程序员就是这么“双标”。 :smile: 15 | 16 | 不过麻烦归麻烦,出来混,文档还是要写的,不然哪天回头翻自己的项目,连自己都不知道写了个啥,就很尴尬了。当然,文档通常是为别人写的,特别是一些工具类的库或者开源软件,从最简单的 readme,到成体系的在线 wiki,再到自建在线文档网站,这大概是很多开源作者都有过的历程。 17 | 18 | 而对于在线文档网站,搜索功能能让查阅文档更加轻松,我也一直想着为自己的文档站搞个搜索功能,但看完一些全文搜索工具的教程后给整懵逼了,也迟迟没正式动手。直到最近发现了这货 —— [Algolia DocSearch](https://community.algolia.com/docsearch/),前后不到 3 小时(包括申请时等待的时间)就弄好了。 19 | 20 | 了却心头大事后,也惊异于它好用,简直是难得的良心软件。如此幸事,岂能不装一逼?…… 21 | 22 | ## Algolia DocSearch 的基本原理和主要优势 23 | 24 | 相对于其它一些全文搜索方案,Algolia DocSearch 的主要优势在于它是专门针对在线文档搜索这一需求的。不需要繁琐的配置,也不需要自己有数据库等软硬件支持,而只需在自己网站中插入少量代码就可以实现强大的文档搜索功能了。 25 | 26 | 根据官方的说明,在你通过申请后,其服务器会定期抓取(免费用户抓取周期是 24 小时)你的网站内容并分析,对文档的各级标题、段落等内容建立索引,这样,在网站中加入搜索框之后,用户输入关键时是便可以请求 DocSearch 的接口并显示搜索结果了。这些请求、结果显示相关的逻辑都封装好了,你要做的只是要按要求插入代码、样式以及那个搜索框。 27 | 28 |  29 | 30 | ## 实现步骤 31 | 32 | 1. 在 [Algolia DocSearch 官网](https://community.algolia.com/docsearch/) 填写自己的文档网站的地址和邮箱进行申请 33 | 34 | DocSearch 可以免费使用,而且不用注册,因为他们觉得,任何人都应该能够有能力构建方便搜索的文档(可以说相当有情怀吧)。当然,也有收费的服务可供选用,差异在于技术支持和请求频率限制等方面不同。 35 | 36 | 2. 收到确认邮件并确认 37 | 38 | 提交申请之后不久,你所填写的邮箱就会收到一封询问邮件。里面说明你的网站技术上是否支持写用 DocSearch。如果支持,还会询问你是否能修改源码向其中注入需要的代码。你需要回复邮件进行确认。 39 | 40 | 3. DocSearch 对你的文档网站首次爬取页面数据,并向你发送需要注入的代码及相关操作指导。 41 | 42 | 第 2 和 第 3 步都需要对方人工处理,而且根据你的网站复杂程序,需要等待的时间会有差异,不过就我个人经验而言还是很快的。前后不到两个小时。 43 | 44 | 邮件内容大致如下: 45 | 46 |  47 | 48 | 4. 根据第 3 步里收到的邮件提示,修改网站代码 49 | 50 | 可以看到,邮件主要包括 apiKey 等配置信息,而且对于如何使用也描述得非常清楚了。系统甚至分析出我网站 url 中使用了 v1_6 和 v2_0 区分不同版本的文档,并为此提供相关的参数 `algoliaOptions: { 'facetFilters': ["version:$VERSION"] }` 以及详细使用例子说明,简直无微不至,催人尿下…… 51 | 52 | 因为自己网站用 vue 单文件组件写的,所以我选择使用 npm 包,而并没有完全照着邮件里来,但这实质是一样的。 53 | 54 | 首先,安装 docsearch.js 包 55 | 56 | ```bash 57 | yarn add -D docsearch.js 58 | ``` 59 | 60 | 然后,修改文档页面组件,加入搜索输入框和 docsearch 初始化代码 61 | 62 | ```html 63 | 64 | 71 | 72 | 73 | 89 | ``` 90 | 91 | > **注意:上面只是最简单的示例。实际上使用可以更灵活,例如装搜索框封装成一个组件,若有兴趣,可前往 [we-vue](https://github.com/tianyong90/we-vue) 查看实际使用情况。** 92 | 93 | 5. 最后根据自己的喜好及需要,调整下搜索框及搜索下拉弹出层的样式,就完工了。下面是最终效果。 94 | 95 |  96 | 97 | ## 总结 98 | 99 | Algolia DocSearch 可以说真如其官网描述的那样,算是目前构建可在线搜索文档的最简单的方式之一了。你只需要关注文档本身,进行少量的配置,其它的 Algolia 全包了。另外,Algolia 还有一些其它优秀产品及服务,诸位可前往官网自行探索。 100 | 101 | 本文以自己的项目为例,但 Aloglia DocSearch 适合很多类型的网站,使用 [Vue.js 官网](https://vuejs.org)这类用 HEXO 构建的静态站,又或者像 [Easywechat](https://easywechat.com) 一样用 Laravel 开发的动态网站(事实上自己早前曾向超哥安利过 DocSearch, 然后竟然真被用上了 :smile: )。有了搜索功能之后,用户能更方便有找到自己想要的信息,当然,网站的格调也极大的提升了! 102 | -------------------------------------------------------------------------------- /content/posts/用-Algolia-DocSearch-轻松实现文档全站搜索.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 用 Algolia DocSearch 轻松实现文档全站搜索 3 | date: 2019-03-10 16:46:31 4 | top_img: /top_img.jpg 5 | tags: 6 | - Algolia DocSearch 7 | - 全站搜索 8 | categories: 9 | - js 10 | draft: false 11 | --- 12 | 13 | 话说,有两件事能让程序员抓狂,一是写文档,二是看别人的代码发现没写文档…… 14 | 15 | 没错,咱程序员就是这么“双标”。 :smile: 16 | 17 | 不过麻烦归麻烦,出来混,文档还是要写的,不然哪天回头翻自己的项目,连自己都不知道写了个啥,就很尴尬了。当然,文档通常是为别人写的,特别是一些工具类的库或者开源软件,从最简单的 readme,到成体系的在线 wiki,再到自建在线文档网站,这大概是很多开源作者都有过的历程。 18 | 19 | 而对于在线文档网站,搜索功能能让查阅文档更加轻松,我也一直想着为自己的文档站搞个搜索功能,但看完一些全文搜索工具的教程后给整懵逼了,也迟迟没正式动手。直到最近发现了这货 —— [Algolia DocSearch](https://community.algolia.com/docsearch/),前后不到 3 小时(包括申请时等待的时间)就弄好了。 20 | 21 | 了却心头大事后,也惊异于它好用,简直是难得的良心软件。如此幸事,岂能不装一逼?…… 22 | 23 | ## Algolia DocSearch 的基本原理和主要优势 24 | 25 | 相对于其它一些全文搜索方案,Algolia DocSearch 的主要优势在于它是专门针对在线文档搜索这一需求的。不需要繁琐的配置,也不需要自己有数据库等软硬件支持,而只需在自己网站中插入少量代码就可以实现强大的文档搜索功能了。 26 | 27 | 根据官方的说明,在你通过申请后,其服务器会定期抓取(免费用户抓取周期是 24 小时)你的网站内容并分析,对文档的各级标题、段落等内容建立索引,这样,在网站中加入搜索框之后,用户输入关键时是便可以请求 DocSearch 的接口并显示搜索结果了。这些请求、结果显示相关的逻辑都封装好了,你要做的只是要按要求插入代码、样式以及那个搜索框。 28 | 29 |  30 | 31 | ## 实现步骤 32 | 33 | 1. 在 [Algolia DocSearch 官网](https://community.algolia.com/docsearch/) 填写自己的文档网站的地址和邮箱进行申请 34 | 35 | DocSearch 可以免费使用,而且不用注册,因为他们觉得,任何人都应该能够有能力构建方便搜索的文档(可以说相当有情怀吧)。当然,也有收费的服务可供选用,差异在于技术支持和请求频率限制等方面不同。 36 | 37 | 2. 收到确认邮件并确认 38 | 39 | 提交申请之后不久,你所填写的邮箱就会收到一封询问邮件。里面说明你的网站技术上是否支持写用 DocSearch。如果支持,还会询问你是否能修改源码向其中注入需要的代码。你需要回复邮件进行确认。 40 | 41 | 3. DocSearch 对你的文档网站首次爬取页面数据,并向你发送需要注入的代码及相关操作指导。 42 | 43 | 第 2 和 第 3 步都需要对方人工处理,而且根据你的网站复杂程序,需要等待的时间会有差异,不过就我个人经验而言还是很快的。前后不到两个小时。 44 | 45 | 邮件内容大致如下: 46 | 47 |  48 | 49 | 4. 根据第 3 步里收到的邮件提示,修改网站代码 50 | 51 | 可以看到,邮件主要包括 apiKey 等配置信息,而且对于如何使用也描述得非常清楚了。系统甚至分析出我网站 url 中使用了 v1_6 和 v2_0 区分不同版本的文档,并为此提供相关的参数 `algoliaOptions: { 'facetFilters': ["version:$VERSION"] }` 以及详细使用例子说明,简直无微不至,催人尿下…… 52 | 53 | 因为自己网站用 vue 单文件组件写的,所以我选择使用 npm 包,而并没有完全照着邮件里来,但这实质是一样的。 54 | 55 | 首先,安装 docsearch.js 包 56 | 57 | ```bash 58 | yarn add -D docsearch.js 59 | ``` 60 | 61 | 然后,修改文档页面组件,加入搜索输入框和 docsearch 初始化代码 62 | 63 | ```html 64 | 65 | 72 | 73 | 74 | 90 | ``` 91 | 92 | > **注意:上面只是最简单的示例。实际上使用可以更灵活,例如装搜索框封装成一个组件,若有兴趣,可前往 [we-vue](https://github.com/tianyong90/we-vue) 查看实际使用情况。** 93 | 94 | 5. 最后根据自己的喜好及需要,调整下搜索框及搜索下拉弹出层的样式,就完工了。下面是最终效果。 95 | 96 |  97 | 98 | ## 总结 99 | 100 | Algolia DocSearch 可以说真如其官网描述的那样,算是目前构建可在线搜索文档的最简单的方式之一了。你只需要关注文档本身,进行少量的配置,其它的 Algolia 全包了。另外,Algolia 还有一些其它优秀产品及服务,诸位可前往官网自行探索。 101 | 102 | 本文以自己的项目为例,但 Aloglia DocSearch 适合很多类型的网站,使用 [Vue.js 官网](https://vuejs.org)这类用 HEXO 构建的静态站,又或者像 [Easywechat](https://easywechat.com) 一样用 Laravel 开发的动态网站(事实上自己早前曾向超哥安利过 DocSearch, 然后竟然真被用上了 :smile: )。有了搜索功能之后,用户能更方便有找到自己想要的信息,当然,网站的格调也极大的提升了! 103 | -------------------------------------------------------------------------------- /pages/posts/_title.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | {{ post.title }} 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 29 | 上一篇 30 | 31 | 37 | 下一篇 38 | 39 | 40 | 41 | 42 | 43 | 114 | 115 | 144 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "田勇(田较瘦)的博客", 6 | "author": "tianyong90", 7 | "scripts": { 8 | "build": "yarn run generate-post-list --production && nuxt build", 9 | "deploy": "push-dir --dir=dist --branch=gh-pages --cleanup --allow-unclean", 10 | "dev": "nuxt", 11 | "generate": "yarn run generate-post-list --production && nuxt generate", 12 | "generate-post-list": "ts-node --project scripts/tsconfig.json scripts/generate-post-list.ts generate", 13 | "lint": "yarn lint:js && yarn lint:style", 14 | "lint:js": "eslint --ext .js,.vue --ignore-path .gitignore .", 15 | "lint:style": "stylelint **/*.{vue,css} --ignore-path .gitignore", 16 | "lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js", 17 | "new": "ts-node --project scripts/tsconfig.json scripts/create-post.ts", 18 | "test": "ts-node --project scripts/tsconfig.json scripts/test.ts" 19 | }, 20 | "dependencies": { 21 | "@mdi/font": "^6.2.95", 22 | "@nuxt/content": "^1.15.1", 23 | "@nuxtjs/axios": "^5.12.5", 24 | "@nuxtjs/dotenv": "^1.4.1", 25 | "@nuxtjs/svg-sprite": "^0.5.2", 26 | "@tailwindcss/aspect-ratio": "^0.4.0", 27 | "@tailwindcss/line-clamp": "^0.3.1", 28 | "@tailwindcss/typography": "^0.5.1", 29 | "anchor": "^1.4.0", 30 | "cross-env": "^7.0.2", 31 | "dayjs": "^1.10.3", 32 | "fuse.js": "^6.4.6", 33 | "github-markdown-css": "^5.0.0", 34 | "graphql": "^15.7.0", 35 | "gray-matter": "^4.0.2", 36 | "highlight.js": "^11.2.0", 37 | "lodash": "^4.17.11", 38 | "lxgw-wenkai-lite-webfont": "^1.1.0", 39 | "nuxt": "^2.15.2", 40 | "prism-themes": "^1.5.0", 41 | "remark-admonitions": "^1.2.1", 42 | "remark-autolink-headings": "^7.0.1", 43 | "remark-emoji": "^3.0.1", 44 | "remark-external-links": "^9.0.1", 45 | "remark-footnotes": "^4.0.1", 46 | "remark-slug": "^7.0.1", 47 | "slugify": "^1.3.4", 48 | "social-share.js": "^1.0.16", 49 | "summarize-markdown": "^0.3.1", 50 | "tailwindcss": "^3.0.22", 51 | "transliteration": "^2.1.8" 52 | }, 53 | "devDependencies": { 54 | "@fullhuman/postcss-purgecss": "^4.0.0", 55 | "@nuxt/typescript-build": "^2.0.0", 56 | "@nuxtjs/eslint-config": "^8.0.0", 57 | "@nuxtjs/markdownit": "^2.0.0", 58 | "@types/color": "^3.0.1", 59 | "@types/fs-extra": "^9.0.1", 60 | "@types/lodash": "^4.14.144", 61 | "@types/node": "^16.11.6", 62 | "@types/yargs": "^17.0.0", 63 | "@typescript-eslint/eslint-plugin": "^5.2.0", 64 | "@typescript-eslint/parser": "^5.2.0", 65 | "@vue/test-utils": "^2.0.0-rc.17", 66 | "address": "^1.1.2", 67 | "autoprefixer": "^10.2.3", 68 | "babel-core": "^7.0.0-bridge.0", 69 | "babel-jest": "^27.2.5", 70 | "babel-plugin-component": "^1.1.1", 71 | "color": "4.2.0", 72 | "copy-webpack-plugin": "^9.0.1", 73 | "default-gateway": "^6.0.2", 74 | "eslint": "^8.1.0", 75 | "eslint-config-standard": "^16.0.2", 76 | "eslint-loader": "^4.0.0", 77 | "eslint-plugin-import": "^2.20.0", 78 | "eslint-plugin-jest": "^26.1.0", 79 | "eslint-plugin-node": "^11.0.0", 80 | "eslint-plugin-nuxt": "^3.1.0", 81 | "eslint-plugin-promise": "^6.0.0", 82 | "eslint-plugin-unicorn": "^40.1.0", 83 | "eslint-plugin-vue": "^8.4.1", 84 | "fs-extra": "^10.0.0", 85 | "heti": "^0.9.2", 86 | "husky": "^7.0.2", 87 | "jest": "^27.2.5", 88 | "lint-staged": "^12.3.4", 89 | "nodemon": "^2.0.2", 90 | "normalize.css": "^8.0.1", 91 | "postcss": "^8.3.11", 92 | "postcss-html": "^1.1.0", 93 | "postcss-import": "13.0.0", 94 | "postcss-loader": "4.1.0", 95 | "postcss-scss": "^4.0.1", 96 | "postcss-url": "10.1.3", 97 | "prettier": "^2.3.0", 98 | "push-dir": "^0.4.1", 99 | "sass": "^1.43.4", 100 | "sass-loader": "^10.0.2", 101 | "stylelint": "^14.0.0", 102 | "stylelint-config-prettier": "^9.0.2", 103 | "stylelint-config-standard": "^25.0.0", 104 | "stylelint-config-standard-scss": "^3.0.0", 105 | "stylelint-order": "^5.0.0", 106 | "ts-node": "^10.4.0", 107 | "vue-jest": "^3.0.4", 108 | "yargs": "^17.0.1" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /posts/ElementUI-radio-小改造.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ElementUI radio 小改造 3 | date: '2019-08-10 16:22:20' 4 | top_img: ./top_img.jpg 5 | tags: 6 | - ElementUI 7 | - el-radio 8 | categories: 9 | - '' 10 | --- 11 | 12 | ElementUI 是自己比较钟爱的一套 vue 组件库,自己好几个项目里都在用它。一直以来这些丰富的组件,让我能快速的搞定各种后台管理页面,极大地提高了工作效率。 13 | 14 | 但是不管什么软件,肯定都没办法称之为完美,而最近的几个小需求中,也发现了 element ui 的一些不足(也可能是因为自己的需求比较奇葩吧)。其中一点就是本文要提到的,radio 绑定对象类型值的问题。 15 | 16 | 具体现象就是,当通过 mapState 方法自动一个计算对象数组,然后将它绑定到 el-radio 上时,el-radio-group 里的 el-radio 无法根据其绑定值正确的显示 checked 状态。 17 | 18 | 例如下面这段代码: 19 | 20 | ```html 21 | 22 | 23 | 26 | 32 | {{ `${user.name}(${user.age}岁)` }} 33 | 34 | 35 | 36 | 当前选中 37 | {{ checkedUser }} 38 | 39 | 40 | 41 | 63 | ``` 64 | 65 | 其中 users 为 vuex store 中的 state。 66 | 67 | ```js 68 | import Vue from 'vue' 69 | import Vuex from 'vuex' 70 | Vue.use(Vuex) 71 | 72 | const store = new Vuex.Store({ 73 | state: { 74 | users: [ 75 | { 76 | name: 'A', 77 | age: 18, 78 | }, 79 | { 80 | name: 'B', 81 | age: 20, 82 | }, 83 | { 84 | name: 'C', 85 | age: 1, 86 | }, 87 | ] 88 | 89 | }, 90 | }) 91 | 92 | export default store 93 | ``` 94 | 95 | 但当运行代码之后看到,第三个 el-radio 并没有像预期的那样处于选中状态。 96 | 97 |  98 | 99 | 查看代码时发现,el-radio 里的 checked 是根据 `this.model === this.label` 来判断的([见代码](https://github.com/ElemeFE/element/blob/4680e55b96613004999f9fdeb8bb7b2419853ee8/packages/radio/src/radio.vue#L9)),而当 this.model 和 this.label 都是对象是,它们必须是引用同一个对象才会“恒等”。 100 | 101 | 得益于 Vue 提供的 extends 属性,我们可以轻松的扩展官方原来的 el-radio 组件,对其稍加改造,就可以解决这个问题。 102 | 103 | ```html 104 | 105 | 120 | 126 | 127 | 141 | 142 | 143 | 144 | {{label}} 145 | 146 | 147 | 148 | 149 | 167 | ``` 168 | 169 | 改造完成后,引用这个组件并替换掉原来模板里用到的 el-radio,刷新页面后会发现,radio 的初始选中状态正常了。 170 | 171 |  172 | 173 | 实际上,el-checkbox/el-checkbox-group 也有类似的问题,也是可以解决的,但看过源码之后,发现 el-checkbox 的一些逻辑与 el-radio 又有不小差别,毕竟它绑定的可能就是对象数组,所以具体处理起来会有些不一样,本文就不具体介绍了,如果各位有兴趣可以自行探索。 174 | -------------------------------------------------------------------------------- /content/posts/ElementUI-radio-小改造.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ElementUI radio 小改造 3 | date: '2019-08-10 16:22:20' 4 | top_img: /top_img.jpg 5 | tags: 6 | - ElementUI 7 | - el-radio 8 | categories: 9 | - '' 10 | draft: false 11 | --- 12 | 13 | ElementUI 是自己比较钟爱的一套 vue 组件库,自己好几个项目里都在用它。一直以来这些丰富的组件,让我能快速的搞定各种后台管理页面,极大地提高了工作效率。 14 | 15 | 但是不管什么软件,肯定都没办法称之为完美,而最近的几个小需求中,也发现了 element ui 的一些不足(也可能是因为自己的需求比较奇葩吧)。其中一点就是本文要提到的,radio 绑定对象类型值的问题。 16 | 17 | 具体现象就是,当通过 mapState 方法自动一个计算对象数组,然后将它绑定到 el-radio 上时,el-radio-group 里的 el-radio 无法根据其绑定值正确的显示 checked 状态。 18 | 19 | 例如下面这段代码: 20 | 21 | ```html 22 | 23 | 24 | 27 | 33 | {{ `${user.name}(${user.age}岁)` }} 34 | 35 | 36 | 37 | 当前选中 38 | {{ checkedUser }} 39 | 40 | 41 | 42 | 64 | ``` 65 | 66 | 其中 users 为 vuex store 中的 state。 67 | 68 | ```js 69 | import Vue from 'vue' 70 | import Vuex from 'vuex' 71 | Vue.use(Vuex) 72 | 73 | const store = new Vuex.Store({ 74 | state: { 75 | users: [ 76 | { 77 | name: 'A', 78 | age: 18, 79 | }, 80 | { 81 | name: 'B', 82 | age: 20, 83 | }, 84 | { 85 | name: 'C', 86 | age: 1, 87 | }, 88 | ] 89 | 90 | }, 91 | }) 92 | 93 | export default store 94 | ``` 95 | 96 | 但当运行代码之后看到,第三个 el-radio 并没有像预期的那样处于选中状态。 97 | 98 |  99 | 100 | 查看代码时发现,el-radio 里的 checked 是根据 `this.model === this.label` 来判断的([见代码](https://github.com/ElemeFE/element/blob/4680e55b96613004999f9fdeb8bb7b2419853ee8/packages/radio/src/radio.vue#L9)),而当 this.model 和 this.label 都是对象是,它们必须是引用同一个对象才会“恒等”。 101 | 102 | 得益于 Vue 提供的 extends 属性,我们可以轻松的扩展官方原来的 el-radio 组件,对其稍加改造,就可以解决这个问题。 103 | 104 | ```html 105 | 106 | 121 | 127 | 128 | 142 | 143 | 144 | 145 | {{label}} 146 | 147 | 148 | 149 | 150 | 168 | ``` 169 | 170 | 改造完成后,引用这个组件并替换掉原来模板里用到的 el-radio,刷新页面后会发现,radio 的初始选中状态正常了。 171 | 172 |  173 | 174 | 实际上,el-checkbox/el-checkbox-group 也有类似的问题,也是可以解决的,但看过源码之后,发现 el-checkbox 的一些逻辑与 el-radio 又有不小差别,毕竟它绑定的可能就是对象数组,所以具体处理起来会有些不一样,本文就不具体介绍了,如果各位有兴趣可以自行探索。 175 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import path, { join } from 'path' 2 | import { Configuration } from '@nuxt/types' 3 | import Sass from 'sass' 4 | import posts from './posts/posts.json' 5 | import address from 'address' 6 | import defaultGateway from 'default-gateway' 7 | 8 | const IS_PRODUCTION = process.env.NODE_ENV === 'production' 9 | 10 | /** 11 | * 获取本地 IP 12 | * 13 | * https://github.com/vuejs/vue-cli/blob/eda18b05424c8c3e6862a7a5e2e15b7513bebbe4/packages/%40vue/cli-service/lib/util/prepareURLs.js#L37 14 | */ 15 | let localIp = 'localhost' 16 | try { 17 | // This can only return an IPv4 address 18 | const result = defaultGateway.v4.sync() 19 | const lanUrlForConfig = address.ip(result && result.interface) 20 | if (lanUrlForConfig) { 21 | // Check if the address is a private ip 22 | // https://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces 23 | if ( 24 | /^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test( 25 | lanUrlForConfig, 26 | ) 27 | ) { 28 | localIp = lanUrlForConfig 29 | } 30 | } 31 | } catch (_e) { 32 | // ignored 33 | } 34 | 35 | const config: Configuration = { 36 | // Disable server-side rendering (https://go.nuxtjs.dev/ssr-mode) 37 | ssr: false, 38 | 39 | // Target (https://go.nuxtjs.dev/config-target) 40 | target: 'static', 41 | 42 | // Auto import components (https://go.nuxtjs.dev/config-components) 43 | components: true, 44 | 45 | /* 46 | ** Headers of the page 47 | */ 48 | head: { 49 | titleTemplate: (title) => { 50 | return title ? `${title} - 田写` : '田写' 51 | }, 52 | meta: [ 53 | { charset: 'utf-8' }, 54 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 55 | { hid: 'description', name: 'description', content: '田勇的博客。技术、生活及其它……' }, 56 | ], 57 | link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], 58 | }, 59 | 60 | /* 61 | ** Customize the progress-bar color 62 | */ 63 | loading: { color: '#ff9b36' }, 64 | 65 | /* 66 | ** Global CSS 67 | */ 68 | css: ['~/assets/scss/app.scss'], 69 | 70 | /* 71 | ** Plugins to load before mounting the App 72 | */ 73 | plugins: [ 74 | { 75 | src: '~/plugins/app-service.ts', 76 | ssr: false, 77 | }, 78 | ], 79 | 80 | /* 81 | ** Nuxt.js modules 82 | */ 83 | modules: [ 84 | // Doc: https://axios.nuxtjs.org/usage 85 | '@nuxtjs/axios', 86 | // https://github.com/nuxt-community/svg-sprite-module 87 | '@nuxtjs/svg-sprite', 88 | '@nuxt/content', 89 | ], 90 | 91 | buildModules: [ 92 | '@nuxt/typescript-build', 93 | // https://go.nuxtjs.dev/tailwindcss 94 | // '@nuxtjs/tailwindcss', 95 | '@nuxtjs/dotenv', 96 | ], 97 | 98 | /* 99 | ** Axios module configuration 100 | */ 101 | axios: { 102 | // See https://github.com/nuxt-community/axios-module#options 103 | }, 104 | 105 | content: { 106 | liveEdit: true, 107 | markdown: { 108 | remarkPlugins: [ 109 | 'remark-emoji', 110 | // 'remark-admonitions', 111 | 'remark-slug', 112 | 'remark-autolink-headings', 113 | 'remark-external-links', 114 | 'remark-footnotes', 115 | ], 116 | 117 | prism: { 118 | theme: 'prism-themes/themes/prism-material-oceanic.css', 119 | }, 120 | }, 121 | }, 122 | 123 | svgSprite: { 124 | // https://github.com/nuxt-community/svg-sprite-module 125 | // manipulate module options 126 | }, 127 | 128 | /* 129 | ** Build configuration 130 | */ 131 | build: { 132 | parallel: false, // 这个设置为 false,因为 extractCSS 为true 时冲突 133 | 134 | // 生产环境下才提取,开发环境下提取可能导致修改样式后无法热替换(hmr) 135 | extractCSS: process.env.NODE_ENV === 'production', 136 | 137 | transpile: ['color'], 138 | 139 | loaders: { 140 | scss: { 141 | implementation: Sass, 142 | }, 143 | }, 144 | 145 | postcss: { 146 | plugins: { 147 | 'postcss-import': {}, 148 | 'postcss-url': {}, 149 | tailwindcss: {}, 150 | autoprefixer: {}, 151 | ...(IS_PRODUCTION ? { cssnano: {} } : {}), 152 | }, 153 | }, 154 | }, 155 | 156 | // TODO: 157 | // generate: { 158 | // routes: ['404'].concat(posts.map(post => `/posts/${post.slugifiedFilename}`)), 159 | // }, 160 | 161 | router: { 162 | mode: 'hash', 163 | }, 164 | 165 | server: { 166 | host: process.env.DEV_SERVER_HOST || localIp, 167 | port: 3000, 168 | }, 169 | 170 | env: { 171 | GIT_TOKEN: process.env.GIT_TOKEN, 172 | }, 173 | } 174 | 175 | export default config 176 | -------------------------------------------------------------------------------- /posts/让-F5-歇一会儿——laravel-mix-自动刷新之道.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 让 F5 歇一会儿——laravel-mix 自动刷新之道 3 | date: 2019-04-12 22:07:38 4 | top_img: ./top_img.jpg 5 | tags: 6 | - laravel-mix 7 | - 前端 8 | - laravel 9 | categories: 10 | - 前端 11 | - webpack 12 | --- 13 | 14 | 转眼入行已五年有余,如今已经成长为一个**全干**程序员。回想起当初使用的一些工具以及工作流,感觉真是笨拙而粗暴,特别是对于浏览器刷新这事儿,只会猛击 F5,不禁感慨那饱经摧残的 F5 键真是坚挺异常,竟没有提前挂掉。 15 | 16 | 随着踩的坑越来越多,也日渐积累了不少经验,这其中就包括各种自动刷新的办法。因为近几年来大部分时假在与 Laravel 打交道,使用 laravel-mix 已成家常便饭,所以想着总结并分享一下 laravel-mix 工作流中的自动刷新之道。 17 | 18 | laravel-mix 自称 `An elegant wrapper around Webpack for the 80% use case`,其功能确实强大,它对于前端开发工作流的考虑也是非常全面,可以通过 `Browsersync`、`Hot Module Replacement` 和 `LiveReload` 实现自动刷新。 19 | 20 | > 在接下来的内容之前,需要说明一下我平时使用的环境。 21 | > 系统为 windows10,前端资源编译调试都在宿主机(即 windows10)中完成,而 php, mysql 等由 laradock 容器提供。 22 | > **我还为此创建了一个[演示项目](https://github.com/tianyong90/laravel-mix-autoreload-demo),文中的几个录屏动画也来自该项目,有兴趣的可自行克隆查看源码。** 23 | 24 | ## Browsersync 25 | 26 | [Browsersync](https://www.browsersync.io/) 是一款强大的前端调试工具,如它的名字一样,主要的功能就是“浏览器同步”,这里的同步不仅是当资源发生变化时同步刷新,它支持局域网中多终端设备同时调试,甚至能同步这些设备上的滚动、点击等操作。此外它还担任了一个易于使用的 UI 界面(页面)以及一些插件,具体信息可前往官网查看。 27 | 28 |  29 | 30 | 1. 安装依赖 31 | 32 | ```bash 33 | yarn add -D browser-sync browser-sync-webpack-plugin 34 | ``` 35 | 36 | 1. 在 `webpack.mix.js` 文件中调用 `mix.browserSync()`启动 Browsersync 37 | 38 | ```js 39 | /** 40 | *下面方法启用 bs,不传参则使用 laravel-mix 的默认配置 41 | * 根据实际使用环境配置参数以获得更好体验 42 | * bs 配置选项参考 https://www.browsersync.io/docs/options 43 | */ 44 | mix.browserSync({ 45 | proxy: 'laravel-mix-autoreload-demo.test/', 46 | startPath: '/demo-bs', 47 | open: true, 48 | reloadOnRestart: true, 49 | watchOptions: { 50 | usePolling: true, 51 | }, 52 | }) 53 | ``` 54 | 55 | 1. 运行 `yarn run watch-poll` 56 | 57 | 如果 Browsersync 的 `open` 选项设置的为 `true`,在首次编译完成之后浏览器会自动打开一个页面,否则需要手动打开,默认的是 http://localhost:3000,具体依所设置的 Browsersync 参数而定。 58 | 59 | 1. 修改相关文件关保存,webpack 将会自动编译修改的文件,完成之后页面将自动刷新。(如果修改的是后端文件,则直接刷新) 60 | 61 |  62 | 63 | ## Hot Module Replacement(hmr) 64 | 65 | 相信熟悉 webpack 的前端 er 都知道 [hmr](https://webpack.js.org/concepts/hot-module-replacement/) 是什么。有别于一般的刷新(即整页相关资源重新加载),它可以只对发生变化的部分模块进行热替换,而其它部分保持不变。这使得它不仅反应及时,通常也能保持当前应用状态不会被刷新,这对于调试 SPA 项目十分方便。当然,并不是所有修改它都能进行热替换,有时也会整页刷新。 66 | 67 | 要在 laravel-mix 中使用 hmr,不需要安装其它额外的依赖包。 68 | 69 | 1. 在 `webpack.mix.js` 中根据实际场景配置 hmr 参数 70 | 71 | ```js 72 | // 配置 hmr 参数 73 | mix.options({ 74 | hmrOptions: { 75 | host: 'laravel-mix-autoreload-demo.test', 76 | port: 8080, 77 | } 78 | }) 79 | ``` 80 | 81 | 1. 执行 `yarn run hot` 82 | 83 | 首次编辑完成之后,打开对应的页面,例如本文提到的示例项目打开 `http://laravel-mix-autoreload-demo.test/demo-hmr` 84 | 85 | 1. 修改前端资源文件,愉快撸码 86 | 87 |  88 | 89 | ## LiveReload 90 | 91 | LiveReload 算是一个比较老(维护更新也不勤)的工具了,关于它的详细介绍请访问[官网](http://livereload.com)。 92 | 93 | 1. 安装依赖 94 | 95 | ```bash 96 | // laravel-mix v4 97 | yarn add -D webpack-livereload-plugin 98 | 99 | // laravel-mix v3 或更早 100 | yarn add -D webpack-livereload-plugin@1 101 | ``` 102 | 103 | 1. 在模板的 body 最后加上额外引用的 js 104 | 105 | ```php 106 | @if(config('app.env') == 'local') 107 | 108 | @endif 109 | ``` 110 | 111 | > 也可以选择安装[浏览器插件](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei)替代 112 | 113 | 1. 执行 `yarn run watch-poll` 114 | 115 | 执行该命令以监听文件变化并让 webpack 自动重新编译。 116 | 117 | 1. 打开页面,修改页面引用的前端资源(如 js,css)并保存,页面将自动刷新 118 | 119 | 因为使用 laravel-mix 编译,一般修改 resource/ 目录下的文件,但实际上直接修改 public/ 目录中的文件也是可以触发刷新的。 120 | 121 |  122 | 123 | ## 三者对比 124 | 125 | | | Browsersync | Hot Module Replacement | LiveReload | 126 | |---|---|---|---| 127 | | 刷新方式 | 修改 css 文件时为部分替换,其它整页刷新 | 模块热替换或整页刷新 | 整页刷新 | 128 | | 监听范围 | 在配置项 files 规则所包含的前后端文件 | 前端模块(即 webpack 加载的模块) | 浏览器当前页面所加载的前端文件 | 129 | | 速度 | 修改 css 时较快,其它文件时一般 | 快,特别是热替换时 | 一般 | 130 | | 可靠性 | 可靠 | 存在 Bug,但有特殊处理办法 | 可靠 | 131 | | 使用复杂度 | 简单,仅需安装依赖并调用 mix.browserSync() 方法 | 较复杂,可能需要针对目前存在的 Bug 作特殊处理 | 较复杂,需要安装依赖,并在入口模板中手动添加额外 js 引用(或使用浏览器插件) | 132 | | 主要优势 | 功能强大,配置灵活,可同时响应前后端文件变化,适合绝大部分场景 | 热替换几乎实现实时预览且不响应应用状态,适合 SPA 项目 | 相对于其它两个似乎没特别优势(至少目前本人未发现 :smile:) | 133 | 134 | ## 个人日常使用习惯 135 | 136 | 因为 Browsersync 的可靠性与广泛适用性,它通常是我开发时使用的主力工具(甚至我为 hexo 与安装的 Browsersync 插件)。 137 | 138 | 而 hmr 我通常只在调试 SPA 项目时使用,因为它响应速度快,而且通常不会影响应用状态,十分方便。但同时需要注意的是 laravel-mix 环境下使用 hmr 也存在一些问题(当前最新版本 4.0.15 中仍存在),例如与 `mix.extract()`没法同时使用([见 Issue](https://github.com/laravel-enso/Enso/issues/194)) 以及在 windows 环境中存在的路径分隔符问题[见 Issue](https://github.com/JeffreyWay/laravel-mix/pull/1995),好在这几个 Issue 里也给出了这些问题的解决办法,虽然不甚优雅,但至少行得通。(**在前面提到的示例项目里有相关的代码及注释,可自行查阅**) 139 | 140 | 至于 LiveReload,我完全不会在日常开发中使用。因为相较于其它两个,它几乎没有什么优势可言,而且维护情况也堪忧。 141 | 142 | ## 总结 143 | 144 | 前端开发花样百出,各种技术、框架以及工具层出不穷。作为一个程序员,当然不得不学习这些,毕竟生命在于折腾,而前端开发尤其如此。庆幸的是有些折腾也是值得的,它能解救我们(或者解救我们的 F5 键 :smile:),例如当你掌握了各种各样的自动刷新方法(包括但不限于本文提及的),你会发现自己临幸 F5 键的频率会越来越低,不知不觉省下来不少时间,可以用来睡觉、逛街、吃鸡或者有娃的带娃…… 145 | -------------------------------------------------------------------------------- /content/posts/让-F5-歇一会儿——laravel-mix-自动刷新之道.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 让 F5 歇一会儿——laravel-mix 自动刷新之道 3 | date: 2019-04-12 22:07:38 4 | top_img: /top_img.jpg 5 | tags: 6 | - laravel-mix 7 | - 前端 8 | - laravel 9 | categories: 10 | - 前端 11 | - webpack 12 | draft: false 13 | --- 14 | 15 | 转眼入行已五年有余,如今已经成长为一个**全干**程序员。回想起当初使用的一些工具以及工作流,感觉真是笨拙而粗暴,特别是对于浏览器刷新这事儿,只会猛击 F5,不禁感慨那饱经摧残的 F5 键真是坚挺异常,竟没有提前挂掉。 16 | 17 | 随着踩的坑越来越多,也日渐积累了不少经验,这其中就包括各种自动刷新的办法。因为近几年来大部分时假在与 Laravel 打交道,使用 laravel-mix 已成家常便饭,所以想着总结并分享一下 laravel-mix 工作流中的自动刷新之道。 18 | 19 | laravel-mix 自称 `An elegant wrapper around Webpack for the 80% use case`,其功能确实强大,它对于前端开发工作流的考虑也是非常全面,可以通过 `Browsersync`、`Hot Module Replacement` 和 `LiveReload` 实现自动刷新。 20 | 21 | > 在接下来的内容之前,需要说明一下我平时使用的环境。 22 | > 系统为 windows10,前端资源编译调试都在宿主机(即 windows10)中完成,而 php, mysql 等由 laradock 容器提供。 23 | > **我还为此创建了一个[演示项目](https://github.com/tianyong90/laravel-mix-autoreload-demo),文中的几个录屏动画也来自该项目,有兴趣的可自行克隆查看源码。** 24 | 25 | ## Browsersync 26 | 27 | [Browsersync](https://www.browsersync.io/) 是一款强大的前端调试工具,如它的名字一样,主要的功能就是“浏览器同步”,这里的同步不仅是当资源发生变化时同步刷新,它支持局域网中多终端设备同时调试,甚至能同步这些设备上的滚动、点击等操作。此外它还担任了一个易于使用的 UI 界面(页面)以及一些插件,具体信息可前往官网查看。 28 | 29 |  30 | 31 | 1. 安装依赖 32 | 33 | ```bash 34 | yarn add -D browser-sync browser-sync-webpack-plugin 35 | ``` 36 | 37 | 1. 在 `webpack.mix.js` 文件中调用 `mix.browserSync()`启动 Browsersync 38 | 39 | ```js 40 | /** 41 | *下面方法启用 bs,不传参则使用 laravel-mix 的默认配置 42 | * 根据实际使用环境配置参数以获得更好体验 43 | * bs 配置选项参考 https://www.browsersync.io/docs/options 44 | */ 45 | mix.browserSync({ 46 | proxy: 'laravel-mix-autoreload-demo.test/', 47 | startPath: '/demo-bs', 48 | open: true, 49 | reloadOnRestart: true, 50 | watchOptions: { 51 | usePolling: true, 52 | }, 53 | }) 54 | ``` 55 | 56 | 1. 运行 `yarn run watch-poll` 57 | 58 | 如果 Browsersync 的 `open` 选项设置的为 `true`,在首次编译完成之后浏览器会自动打开一个页面,否则需要手动打开,默认的是 http://localhost:3000,具体依所设置的 Browsersync 参数而定。 59 | 60 | 1. 修改相关文件关保存,webpack 将会自动编译修改的文件,完成之后页面将自动刷新。(如果修改的是后端文件,则直接刷新) 61 | 62 |  63 | 64 | ## Hot Module Replacement(hmr) 65 | 66 | 相信熟悉 webpack 的前端 er 都知道 [hmr](https://webpack.js.org/concepts/hot-module-replacement/) 是什么。有别于一般的刷新(即整页相关资源重新加载),它可以只对发生变化的部分模块进行热替换,而其它部分保持不变。这使得它不仅反应及时,通常也能保持当前应用状态不会被刷新,这对于调试 SPA 项目十分方便。当然,并不是所有修改它都能进行热替换,有时也会整页刷新。 67 | 68 | 要在 laravel-mix 中使用 hmr,不需要安装其它额外的依赖包。 69 | 70 | 1. 在 `webpack.mix.js` 中根据实际场景配置 hmr 参数 71 | 72 | ```js 73 | // 配置 hmr 参数 74 | mix.options({ 75 | hmrOptions: { 76 | host: 'laravel-mix-autoreload-demo.test', 77 | port: 8080, 78 | } 79 | }) 80 | ``` 81 | 82 | 1. 执行 `yarn run hot` 83 | 84 | 首次编辑完成之后,打开对应的页面,例如本文提到的示例项目打开 `http://laravel-mix-autoreload-demo.test/demo-hmr` 85 | 86 | 1. 修改前端资源文件,愉快撸码 87 | 88 |  89 | 90 | ## LiveReload 91 | 92 | LiveReload 算是一个比较老(维护更新也不勤)的工具了,关于它的详细介绍请访问[官网](http://livereload.com)。 93 | 94 | 1. 安装依赖 95 | 96 | ```bash 97 | // laravel-mix v4 98 | yarn add -D webpack-livereload-plugin 99 | 100 | // laravel-mix v3 或更早 101 | yarn add -D webpack-livereload-plugin@1 102 | ``` 103 | 104 | 1. 在模板的 body 最后加上额外引用的 js 105 | 106 | ```php 107 | @if(config('app.env') == 'local') 108 | 109 | @endif 110 | ``` 111 | 112 | > 也可以选择安装[浏览器插件](https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei)替代 113 | 114 | 1. 执行 `yarn run watch-poll` 115 | 116 | 执行该命令以监听文件变化并让 webpack 自动重新编译。 117 | 118 | 1. 打开页面,修改页面引用的前端资源(如 js,css)并保存,页面将自动刷新 119 | 120 | 因为使用 laravel-mix 编译,一般修改 resource/ 目录下的文件,但实际上直接修改 public/ 目录中的文件也是可以触发刷新的。 121 | 122 |  123 | 124 | ## 三者对比 125 | 126 | | | Browsersync | Hot Module Replacement | LiveReload | 127 | |---|---|---|---| 128 | | 刷新方式 | 修改 css 文件时为部分替换,其它整页刷新 | 模块热替换或整页刷新 | 整页刷新 | 129 | | 监听范围 | 在配置项 files 规则所包含的前后端文件 | 前端模块(即 webpack 加载的模块) | 浏览器当前页面所加载的前端文件 | 130 | | 速度 | 修改 css 时较快,其它文件时一般 | 快,特别是热替换时 | 一般 | 131 | | 可靠性 | 可靠 | 存在 Bug,但有特殊处理办法 | 可靠 | 132 | | 使用复杂度 | 简单,仅需安装依赖并调用 mix.browserSync() 方法 | 较复杂,可能需要针对目前存在的 Bug 作特殊处理 | 较复杂,需要安装依赖,并在入口模板中手动添加额外 js 引用(或使用浏览器插件) | 133 | | 主要优势 | 功能强大,配置灵活,可同时响应前后端文件变化,适合绝大部分场景 | 热替换几乎实现实时预览且不响应应用状态,适合 SPA 项目 | 相对于其它两个似乎没特别优势(至少目前本人未发现 :smile:) | 134 | 135 | ## 个人日常使用习惯 136 | 137 | 因为 Browsersync 的可靠性与广泛适用性,它通常是我开发时使用的主力工具(甚至我为 hexo 与安装的 Browsersync 插件)。 138 | 139 | 而 hmr 我通常只在调试 SPA 项目时使用,因为它响应速度快,而且通常不会影响应用状态,十分方便。但同时需要注意的是 laravel-mix 环境下使用 hmr 也存在一些问题(当前最新版本 4.0.15 中仍存在),例如与 `mix.extract()`没法同时使用([见 Issue](https://github.com/laravel-enso/Enso/issues/194)) 以及在 windows 环境中存在的路径分隔符问题[见 Issue](https://github.com/JeffreyWay/laravel-mix/pull/1995),好在这几个 Issue 里也给出了这些问题的解决办法,虽然不甚优雅,但至少行得通。(**在前面提到的示例项目里有相关的代码及注释,可自行查阅**) 140 | 141 | 至于 LiveReload,我完全不会在日常开发中使用。因为相较于其它两个,它几乎没有什么优势可言,而且维护情况也堪忧。 142 | 143 | ## 总结 144 | 145 | 前端开发花样百出,各种技术、框架以及工具层出不穷。作为一个程序员,当然不得不学习这些,毕竟生命在于折腾,而前端开发尤其如此。庆幸的是有些折腾也是值得的,它能解救我们(或者解救我们的 F5 键 :smile:),例如当你掌握了各种各样的自动刷新方法(包括但不限于本文提及的),你会发现自己临幸 F5 键的频率会越来越低,不知不觉省下来不少时间,可以用来睡觉、逛街、吃鸡或者有娃的带娃…… 146 | -------------------------------------------------------------------------------- /posts/Laravel-laravel-echo-EasyWeChat-实现微信扫码登录.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Laravel + laravel-echo + EasyWeChat 实现微信扫码登录 3 | date: 2019-03-19 16:25:48 4 | top_img: ./top_img.jpg 5 | tags: 6 | - php 7 | - laravel 8 | - EasyWeChat 9 | categories: 10 | - Laravel 11 | --- 12 | 13 | 扫码登录成为一种日趋流行的登录方式,它具有较高的安全性,同时又使我们从记忆大量的账号密码并手动输入的繁琐流程中解脱出来,有些平台甚至无账号也能扫码登录,连注册的麻烦都省了。 14 | 15 | 对于接入微信开放平台的公众号应用来说,实现扫码登录是相当容易的,有 EasyWeChat SDK 加持,再按着官方的文档一把梭,很快就能完成。 16 | 然而本文所要讨论的是另一种情况,有时候出于某些原因,自己的公众号不能接入开放平台,但又想进行微信扫码登录,这种情况下显示就不能再换官方的套路来了。但只要我们稍作变通,就能实现这一需求。 17 | 18 | ## 基本思路: 19 | 20 | 1. 登录页显示微信二维码(使用 EasyWeChat SDK 创建,短时效的临时二维码) 21 | 2. 用户扫码后推送消息到服务器接口,接口中根据业务情况进行判断处理,符合条件时触发 WechatScanLogin 事件 22 | 3. WechatScanLogin 事件实现 ShouldBroadcast 接口,所以当它被触发时也会向指定的频道进行广播 23 | 4. 前端 laravel-echo 监听频道中用户扫码登录的消息并进行处理 24 | 25 | 以下就来介绍一下具体实现,先放效果图。 26 | 27 |  28 | 29 | ## 具体实现 30 | 31 | > 配合本文,我创建了一个简单的示例项目,有兴趣的可以克隆下来,配合源码一起服用,效果更佳。项目地址:[https://github.com/tianyong90/laravel-qrcode-login](https://github.com/tianyong90/laravel-qrcode-login) 32 | 33 | 1. 首先当然是创建 Laravel 项目,同时安装前后端依赖 34 | 35 | 前端最主要依赖是 `laravel-echo` 和 `socket.io-client` 36 | 37 | > 前端监听事件广播是关键,我们需要一个 websocket 服务端,Laravel 官方文档在介绍消息广播时提到了 Pusher 和 laravel-echo-server。因为我使用 laradock 作为开发环境,其中内置了 laravel-echo-server 容器,十分方便,所以决定直接用它。实际上也可以使用 Pusher 服务,那么则需要安装 pusher.js 替代 socket.io-client,同时在 .env 中修改相关配置 38 | 39 | 2. 配置项目 40 | 41 | 主要是配置数据库和 redis 连接,然后把 BROADCAST_DRIVER 设置为 redis(这一点很重要,如果使用 pusher 则需要修改为 pusher) 42 | 43 | 如果 QUEUE_CONNECTION 设置为 redis 了,则需要记得启动队列 worker. 44 | 45 | 3. 启动 laravel-echo-server 46 | 47 | 因为使用 laradock,所以只需要启动时带上 laravel-echo-server 参数就可以了,进入 laradock 目录 48 | 49 | ```bash 50 | docker-compose up -d nginx php-worker nginx mysql redis laravel-echo-server 51 | ``` 52 | 53 | 4. 创建 WechatScanLogin 事件 54 | 55 | ```shell 56 | php artisan make:event WechatScanLogin 57 | ``` 58 | 59 | ```php 60 | class WechatScanLogin implements ShouldBroadcast 61 | { 62 | use Dispatchable, InteractsWithSockets, SerializesModels; 63 | 64 | public $token; 65 | 66 | /** 67 | * Create a new event instance. 68 | * 69 | * @param $token 70 | */ 71 | public function __construct($token) 72 | { 73 | $this->token = $token; 74 | } 75 | 76 | /** 77 | * Get the channels the event should broadcast on. 78 | * 79 | * @return \Illuminate\Broadcasting\Channel|array 80 | */ 81 | public function broadcastOn() 82 | { 83 | return new Channel(‘scan-login’); 84 | } 85 | } 86 | ``` 87 | 88 | > 上面最关键的就是事件要实现 ShouldBroadcast 接口并在 broadcastOn 方法中指定要广播的频道。WechatScanLogin 的公开属性 token 会自动包含在广播数据中。 89 | 90 | 5. 对接微信消息服务器 91 | 92 | > laravel-wechat 的相关配置和对接,请阅读 EasyWeChat SDK 官方文档。 93 | 94 | 接收扫码的消息并进行相关处理。 95 | 96 | ```php 97 | public function serve() 98 | { 99 | $app = app('wechat.official_account'); 100 | 101 | $app->server->push(function ($message) { 102 | if ($message['Event'] === 'SCAN') { 103 | $openid = $message['FromUserName']; 104 | 105 | $user = User::where('openid', $openid)->first(); 106 | 107 | if ($user) { 108 | // TODO: 这里根据情况加入其它鉴权逻辑 109 | 110 | // 使用 laravel-passport 的个人访问令牌 111 | $token = $user->createToken($user->name)->accessToken; 112 | 113 | // 广播扫码登录的消息,以便前端处理 114 | event(new WechatScanLogin($token)); 115 | 116 | \Log::info('haha login'); 117 | return '登录成功!'; 118 | } 119 | 120 | return '失败鸟'; 121 | } else { 122 | // TODO: 用户不存在时,可以直接回返登录失败,也可以创建新的用户并登录该用户再返回 123 | return '登录失败'; 124 | } 125 | }, \EasyWeChat\Kernel\Messages\Message::EVENT); 126 | 127 | return $app->server->serve(); 128 | } 129 | ``` 130 | 131 | 6. 使用 EasyWeChat 创建临时二维码并在页面中显示。 132 | 133 | ```php 134 | public function index() 135 | { 136 | $wechat = app('wechat.official_account'); 137 | 138 | $result = $wechat->qrcode->temporary('foo', 600); 139 | $qrcodeUrl = $wechat->qrcode->url($result['ticket']); 140 | 141 | return view('index', compact('qrcodeUrl')); 142 | } 143 | ``` 144 | 145 | ```html 146 | > 147 | ``` 148 | 149 | 7. 前端使用 laravel-echo 订阅对应的微信扫码登录事件,接收其中的 token 并存入本地存储作为判断是否登录的凭据,同时这个 token 也将作为访问后端 api 的授权依据。注意前面的代码中,使用了 laravel-passport 生成这个个人访问令牌,如果不了解这部分原理,请查阅 Laravel 官方文档。 150 | 151 | ```js 152 | import Echo from 'laravel-echo' 153 | import io from 'socket.io-client' 154 | 155 | window.io = io 156 | 157 | let EchoInstance = new Echo({ 158 | broadcaster: 'socket.io', 159 | host: window.location.hostname + ':6001', 160 | }) 161 | 162 | EchoInstance.channel('scan-login').listen('WechatScanLogin', e => { 163 | localStorage.setItem('my_token', this.token) 164 | 165 | // 其它处理 166 | }) 167 | ``` 168 | 169 | ## 总结 170 | 171 | 至此,简单的扫码登录就完成了。当然,本文示例代码不怎么优雅、流程可能也有不完善的地方,主要是为了提供一个大致思路。有了这个思路,我们可以实现其它诸如扫码签到、扫码投票等各种功能,具体如何就看大家的创意了。 172 | -------------------------------------------------------------------------------- /content/posts/Laravel-laravel-echo-EasyWeChat-实现微信扫码登录.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Laravel + laravel-echo + EasyWeChat 实现微信扫码登录 3 | date: 2019-03-19 16:25:48 4 | top_img: /top_img.jpg 5 | tags: 6 | - php 7 | - laravel 8 | - EasyWeChat 9 | categories: 10 | - Laravel 11 | draft: false 12 | --- 13 | 14 | 扫码登录成为一种日趋流行的登录方式,它具有较高的安全性,同时又使我们从记忆大量的账号密码并手动输入的繁琐流程中解脱出来,有些平台甚至无账号也能扫码登录,连注册的麻烦都省了。 15 | 16 | 对于接入微信开放平台的公众号应用来说,实现扫码登录是相当容易的,有 EasyWeChat SDK 加持,再按着官方的文档一把梭,很快就能完成。 17 | 然而本文所要讨论的是另一种情况,有时候出于某些原因,自己的公众号不能接入开放平台,但又想进行微信扫码登录,这种情况下显示就不能再换官方的套路来了。但只要我们稍作变通,就能实现这一需求。 18 | 19 | ## 基本思路: 20 | 21 | 1. 登录页显示微信二维码(使用 EasyWeChat SDK 创建,短时效的临时二维码) 22 | 2. 用户扫码后推送消息到服务器接口,接口中根据业务情况进行判断处理,符合条件时触发 WechatScanLogin 事件 23 | 3. WechatScanLogin 事件实现 ShouldBroadcast 接口,所以当它被触发时也会向指定的频道进行广播 24 | 4. 前端 laravel-echo 监听频道中用户扫码登录的消息并进行处理 25 | 26 | 以下就来介绍一下具体实现,先放效果图。 27 | 28 |  29 | 30 | ## 具体实现 31 | 32 | > 配合本文,我创建了一个简单的示例项目,有兴趣的可以克隆下来,配合源码一起服用,效果更佳。项目地址:[https://github.com/tianyong90/laravel-qrcode-login](https://github.com/tianyong90/laravel-qrcode-login) 33 | 34 | 1. 首先当然是创建 Laravel 项目,同时安装前后端依赖 35 | 36 | 前端最主要依赖是 `laravel-echo` 和 `socket.io-client` 37 | 38 | > 前端监听事件广播是关键,我们需要一个 websocket 服务端,Laravel 官方文档在介绍消息广播时提到了 Pusher 和 laravel-echo-server。因为我使用 laradock 作为开发环境,其中内置了 laravel-echo-server 容器,十分方便,所以决定直接用它。实际上也可以使用 Pusher 服务,那么则需要安装 pusher.js 替代 socket.io-client,同时在 .env 中修改相关配置 39 | 40 | 2. 配置项目 41 | 42 | 主要是配置数据库和 redis 连接,然后把 BROADCAST_DRIVER 设置为 redis(这一点很重要,如果使用 pusher 则需要修改为 pusher) 43 | 44 | 如果 QUEUE_CONNECTION 设置为 redis 了,则需要记得启动队列 worker. 45 | 46 | 3. 启动 laravel-echo-server 47 | 48 | 因为使用 laradock,所以只需要启动时带上 laravel-echo-server 参数就可以了,进入 laradock 目录 49 | 50 | ```bash 51 | docker-compose up -d nginx php-worker nginx mysql redis laravel-echo-server 52 | ``` 53 | 54 | 4. 创建 WechatScanLogin 事件 55 | 56 | ```shell 57 | php artisan make:event WechatScanLogin 58 | ``` 59 | 60 | ```php 61 | class WechatScanLogin implements ShouldBroadcast 62 | { 63 | use Dispatchable, InteractsWithSockets, SerializesModels; 64 | 65 | public $token; 66 | 67 | /** 68 | * Create a new event instance. 69 | * 70 | * @param $token 71 | */ 72 | public function __construct($token) 73 | { 74 | $this->token = $token; 75 | } 76 | 77 | /** 78 | * Get the channels the event should broadcast on. 79 | * 80 | * @return \Illuminate\Broadcasting\Channel|array 81 | */ 82 | public function broadcastOn() 83 | { 84 | return new Channel(‘scan-login’); 85 | } 86 | } 87 | ``` 88 | 89 | > 上面最关键的就是事件要实现 ShouldBroadcast 接口并在 broadcastOn 方法中指定要广播的频道。WechatScanLogin 的公开属性 token 会自动包含在广播数据中。 90 | 91 | 5. 对接微信消息服务器 92 | 93 | > laravel-wechat 的相关配置和对接,请阅读 EasyWeChat SDK 官方文档。 94 | 95 | 接收扫码的消息并进行相关处理。 96 | 97 | ```php 98 | public function serve() 99 | { 100 | $app = app('wechat.official_account'); 101 | 102 | $app->server->push(function ($message) { 103 | if ($message['Event'] === 'SCAN') { 104 | $openid = $message['FromUserName']; 105 | 106 | $user = User::where('openid', $openid)->first(); 107 | 108 | if ($user) { 109 | // TODO: 这里根据情况加入其它鉴权逻辑 110 | 111 | // 使用 laravel-passport 的个人访问令牌 112 | $token = $user->createToken($user->name)->accessToken; 113 | 114 | // 广播扫码登录的消息,以便前端处理 115 | event(new WechatScanLogin($token)); 116 | 117 | \Log::info('haha login'); 118 | return '登录成功!'; 119 | } 120 | 121 | return '失败鸟'; 122 | } else { 123 | // TODO: 用户不存在时,可以直接回返登录失败,也可以创建新的用户并登录该用户再返回 124 | return '登录失败'; 125 | } 126 | }, \EasyWeChat\Kernel\Messages\Message::EVENT); 127 | 128 | return $app->server->serve(); 129 | } 130 | ``` 131 | 132 | 6. 使用 EasyWeChat 创建临时二维码并在页面中显示。 133 | 134 | ```php 135 | public function index() 136 | { 137 | $wechat = app('wechat.official_account'); 138 | 139 | $result = $wechat->qrcode->temporary('foo', 600); 140 | $qrcodeUrl = $wechat->qrcode->url($result['ticket']); 141 | 142 | return view('index', compact('qrcodeUrl')); 143 | } 144 | ``` 145 | 146 | ```html 147 | > 148 | ``` 149 | 150 | 7. 前端使用 laravel-echo 订阅对应的微信扫码登录事件,接收其中的 token 并存入本地存储作为判断是否登录的凭据,同时这个 token 也将作为访问后端 api 的授权依据。注意前面的代码中,使用了 laravel-passport 生成这个个人访问令牌,如果不了解这部分原理,请查阅 Laravel 官方文档。 151 | 152 | ```js 153 | import Echo from 'laravel-echo' 154 | import io from 'socket.io-client' 155 | 156 | window.io = io 157 | 158 | let EchoInstance = new Echo({ 159 | broadcaster: 'socket.io', 160 | host: window.location.hostname + ':6001', 161 | }) 162 | 163 | EchoInstance.channel('scan-login').listen('WechatScanLogin', e => { 164 | localStorage.setItem('my_token', this.token) 165 | 166 | // 其它处理 167 | }) 168 | ``` 169 | 170 | ## 总结 171 | 172 | 至此,简单的扫码登录就完成了。当然,本文示例代码不怎么优雅、流程可能也有不完善的地方,主要是为了提供一个大致思路。有了这个思路,我们可以实现其它诸如扫码签到、扫码投票等各种功能,具体如何就看大家的创意了。 173 | -------------------------------------------------------------------------------- /posts/绳命在于折腾-我用-Nuxt-js-重构了博客.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 绳命在于折腾,我用 Nuxt.js 重构了博客 3 | date: 2019-05-26 03:11:46 4 | top_img: ./top_img.png 5 | tags: 6 | - 博客 7 | - Nuxt.js 8 | categories: 9 | - Vue 10 | --- 11 | 12 | [博客地址: https://tianyong90.com](https://tianyong90.com/) 13 | 14 | [github 仓库地址:https://github.com/tianyong90/blog](https://github.com/tianyong90/blog) 15 | 16 | 其实自己的博客上线没多久,之前闲时会写些乱七八糟的玩意儿,一来当作总结和备忘,二来分享一些个人经验,也是种很有趣的经历。然后几个月前,想着自己手里有个注册但闲置很久的域名,又正好有台服务器,就干脆折腾个博客。 17 | 18 | 不就是个博客嘛?能有多难?也没多想就用了之前使用过的 Hexo 撸了起来,只花了一晚上就弄上线了。不过上线一时爽,维护火葬场。之后花上它上面的时间要远多于此,因为 hexo 如果想要充分的自定义模板或者功能,还是很麻烦的,特别是因为模板用的 pug 以及写样式用的 stylus 都是自己不擅长且不太喜欢的语言。几番折腾下来总不得劲,终于心一横,不如重构吧。 19 | 20 | 重构时要比当初选择 hexo 时要谨慎多了,对比了下自己了解的一些工具,最终选择了 Nuxt.js。 21 | 22 | ## 为什么是 Nuxt.js? 23 | 24 | 最主要的原因就是自己用 Vue 很久了,学 Nuxt 的成本也就小得多。Nuxt 可以让我用最熟悉的姿势来写代码,同时又能解决博客在静态化、SEO 等方面的一些要求,它的布局、自动路由、插件、中间件等特性让我大有相见恨晚的感觉。 25 | 26 | 其实在作决定之前也试过 Vuepress,但 Vuepress 的出发点是文档类的站,并不太适合写博客。虽然 1.0 版中加入了博客的支持,但目前仍在 alpha 阶段,体验不太好,更新进度又不理想,等到正式稳定可用的版本出来,估计黄花菜也凉了。 27 | 28 | 此外也考虑过用 hugo,甚至想过用 Laravel 来弄。但 hugo 基于 Go,自己完全不懂,而用 Laravel 写博客似乎大材小用了,毕竟我只需要一个静态的小站,也不会给服务器增加多少压力。 29 | 30 | ## 具体实施 31 | 32 | 1. 创建 nuxt 项目并进行基础配置 33 | 34 | 首先当然是创建项目,根据 Nuxt 文档使用 `yarn create nuxt-app` 命令创建一个新项目,根据需要配置好 eslint、typescript 等。 35 | 36 | 2. 确定目录结构(路由)、文章文件名命名规范 37 | 38 | 因为之前用 hexo 部署的也是纯静态的站,只要之前所部署的旧文件不删除,那么使用原来的链接仍然能访问旧版的文章。所以也不用太纠结重构后路由的变化。当然这并不意味着不需要进行规划。 39 | 40 | 为此,我新建了一个 posts 目录,用于保存 markdown 文件,文件夹内建与 markdown 文件同名的文件夹用来存文章中用到的图片等。 41 | 42 | ``` 43 | -| posts/ 44 | ----| hello-中国/ 45 | ----| hello-中国.md 46 | ``` 47 | > 这里要注意一下的是,文件名将一些特殊字符和空格替换成了连词符,而实际访问用的路由是将文件名拼音化。为什么不直接用拼音化文件名或者英文呢?主要是方便日后管理。 48 | 49 | 然后在 `pages` 目录下创建 `psots/_slug.vue` 页面。这样文章就可以用 https://tianyong90.com/psots/hello-zhong-guo 这样形式来访问了。 50 | 51 | 52 | 3. 安装并配置 [@tianyong90/vue-markdown-loader](https://github.com/tianyong90/vue-markdown-loader) 53 | 54 | [@tianyong90/vue-markdown-loader](https://github.com/tianyong90/vue-markdown-loader) 是自己之前从 vuepress 中提取的 markdown-loader 部分代码改写出来的一个 webpack loader。它的主要功能是加载 markdown 文件,进行一些处理,如解析 emoji、代码高亮等,最后返回可以供 vue-loader 的内容。最近又进一步优化,让它可以返回 html 而被 html-loader 处理,或者直接返回一个包含 markdown 文件信息的对象。 55 | 56 | 配置如下: 57 | 58 | ```js 59 | build: { 60 | extend(config, ctx) { 61 | // frontmatter-markdown-loader 62 | config.module!.rules.push({ 63 | test: /\.md$/, 64 | include: path.resolve(__dirname, 'posts'), 65 | use: [ 66 | { 67 | loader: '@tianyong90/vue-markdown-loader', 68 | options: { 69 | mode: 'raw', // 这里表示 import md 文件后直接返回一个对象 70 | contentCssClass: 'markdown-body', 71 | markdown: { 72 | lineNumbers: true, // enable line numbers 73 | }, 74 | }, 75 | }, 76 | ], 77 | }) 78 | }, 79 | } 80 | ``` 81 | 82 | 4. 文章页的一些处理 83 | 84 | 有了前面的这些,就可以开始动手处理文章页了,这也是博客的关键部分。而其中最为重要的工作就是根据 url 中拼音化的文章标题正确加载 posts 目录中的 markdown 链接半渲染显示,这些基本都在 asyncData 方法中完成。 85 | 86 | ```ts 87 | async asyncData({ params }) { 88 | // 这里的 posts.json 是用脚本生成的保存 posts 目录中文章列表信息的 89 | // 相当于一个小的数据库 90 | const { default: posts } = await import('~/posts/posts.json') 91 | 92 | // 链接中拼音化的文件名 93 | const slugifiedFilename = params.slug 94 | 95 | const thePost: any = posts.find((item: any) => { 96 | return item.slugifiedFilename === slugifiedFilename 97 | }) 98 | 99 | // posts 目录中 markdown 实际文件名 100 | const filename = thePost.filename 101 | 102 | // 解析渲染都交给前面提到的 @tianyong90/vue-markdown-loader 完成 103 | // 这里的 html 就是渲染出来的 html,可以直接应用于 v-html 指令 104 | // attributes 则是 markdown 文件头部的 frontmatter 数据如标题、日期等 105 | const { html, attributes } = await import(`~/posts/${filename}.md`) 106 | 107 | return { 108 | ...attributes, 109 | html: html.replace(/src="\.\//g, `src="/_nuxt/posts/${filename}/`), // markdown 内容中图片地址引用替换 110 | } 111 | }, 112 | ``` 113 | 114 | 然后在模板中显示这些数据,其中 html 使用 v-html 指令就可以了。 115 | 116 | ```html 117 | 118 | 119 | 120 | 121 | {{ date }} 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | ``` 132 | 133 | 5. 布局、样式等细节 134 | 135 | 博客并不只是文字内容,因此还需要在布局、样式等方面下些功夫。因为自己设计水平实在有限,所以直接使用了 bootstrap 和 github-markdown-css,撸完文章列表页以及文章内容页就够用了,其它的页面看需要再加吧。 136 | 137 | 6. 生成、部署以及自动化 138 | 139 | 最后要生成静态页,而博客所使用的又是动态路由,就需要在 nuxt.config.js 中的 genarate 荐中进行配置。 140 | 141 | 如下,根据当前 posts 目录下的 markdown 文件名来确定该生成哪些页面。 142 | 143 | ```js 144 | generate: { 145 | routes: ['404'].concat(posts.map(post => `/posts/${post.slugifiedFilename}`)), 146 | }, 147 | ``` 148 | 149 | 执行 `yarn run generate` 后可以看到下面的结果,`dist` 目录里也出现了静态文件,剩下的就只是部署了。 150 | 151 |  152 | 153 | 对于部署,配置上 Circle CI,当推关新内容上 master 分支时由 CI 进行构建并部署到自己服务器简直不能更爽。 154 | 155 | 此外,为了省事,还写了几个脚本来创建新的 markdown 文件和相应的文件夹,虽然这也不是必须的,但使用脚本显然要比手动创建要省事得多。 156 | 157 | ## 总结 158 | 159 | Nuxt.js 确实是个好东西,写了近三年 Vue 了才开始盘它,确实是有点儿迟了。Nuxt 利用 SSR 等机制能很好地弥补 SPA 应用在 SEO 等方面的不足,其自带的生成静态站的功能也非常适合平时写一些博客之类的应用。 160 | 161 | 感谢 Hexo 陪伴多年(虽然期间也没用它写出什么东西来),但以后可能不会再用它了…… 😄 162 | -------------------------------------------------------------------------------- /content/posts/绳命在于折腾-我用-Nuxt-js-重构了博客.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 绳命在于折腾,我用 Nuxt.js 重构了博客 3 | date: 2019-05-26 03:11:46 4 | top_img: /top_img.png 5 | tags: 6 | - 博客 7 | - Nuxt.js 8 | categories: 9 | - Vue 10 | draft: false 11 | --- 12 | 13 | [博客地址: https://tianyong90.com](https://tianyong90.com/) 14 | 15 | [github 仓库地址:https://github.com/tianyong90/blog](https://github.com/tianyong90/blog) 16 | 17 | 其实自己的博客上线没多久,之前闲时会写些乱七八糟的玩意儿,一来当作总结和备忘,二来分享一些个人经验,也是种很有趣的经历。然后几个月前,想着自己手里有个注册但闲置很久的域名,又正好有台服务器,就干脆折腾个博客。 18 | 19 | 不就是个博客嘛?能有多难?也没多想就用了之前使用过的 Hexo 撸了起来,只花了一晚上就弄上线了。不过上线一时爽,维护火葬场。之后花上它上面的时间要远多于此,因为 hexo 如果想要充分的自定义模板或者功能,还是很麻烦的,特别是因为模板用的 pug 以及写样式用的 stylus 都是自己不擅长且不太喜欢的语言。几番折腾下来总不得劲,终于心一横,不如重构吧。 20 | 21 | 重构时要比当初选择 hexo 时要谨慎多了,对比了下自己了解的一些工具,最终选择了 Nuxt.js。 22 | 23 | ## 为什么是 Nuxt.js? 24 | 25 | 最主要的原因就是自己用 Vue 很久了,学 Nuxt 的成本也就小得多。Nuxt 可以让我用最熟悉的姿势来写代码,同时又能解决博客在静态化、SEO 等方面的一些要求,它的布局、自动路由、插件、中间件等特性让我大有相见恨晚的感觉。 26 | 27 | 其实在作决定之前也试过 Vuepress,但 Vuepress 的出发点是文档类的站,并不太适合写博客。虽然 1.0 版中加入了博客的支持,但目前仍在 alpha 阶段,体验不太好,更新进度又不理想,等到正式稳定可用的版本出来,估计黄花菜也凉了。 28 | 29 | 此外也考虑过用 hugo,甚至想过用 Laravel 来弄。但 hugo 基于 Go,自己完全不懂,而用 Laravel 写博客似乎大材小用了,毕竟我只需要一个静态的小站,也不会给服务器增加多少压力。 30 | 31 | ## 具体实施 32 | 33 | 1. 创建 nuxt 项目并进行基础配置 34 | 35 | 首先当然是创建项目,根据 Nuxt 文档使用 `yarn create nuxt-app` 命令创建一个新项目,根据需要配置好 eslint、typescript 等。 36 | 37 | 2. 确定目录结构(路由)、文章文件名命名规范 38 | 39 | 因为之前用 hexo 部署的也是纯静态的站,只要之前所部署的旧文件不删除,那么使用原来的链接仍然能访问旧版的文章。所以也不用太纠结重构后路由的变化。当然这并不意味着不需要进行规划。 40 | 41 | 为此,我新建了一个 posts 目录,用于保存 markdown 文件,文件夹内建与 markdown 文件同名的文件夹用来存文章中用到的图片等。 42 | 43 | ``` 44 | -| posts/ 45 | ----| hello-中国/ 46 | ----| hello-中国.md 47 | ``` 48 | > 这里要注意一下的是,文件名将一些特殊字符和空格替换成了连词符,而实际访问用的路由是将文件名拼音化。为什么不直接用拼音化文件名或者英文呢?主要是方便日后管理。 49 | 50 | 然后在 `pages` 目录下创建 `psots/_slug.vue` 页面。这样文章就可以用 https://tianyong90.com/psots/hello-zhong-guo 这样形式来访问了。 51 | 52 | 53 | 3. 安装并配置 [@tianyong90/vue-markdown-loader](https://github.com/tianyong90/vue-markdown-loader) 54 | 55 | [@tianyong90/vue-markdown-loader](https://github.com/tianyong90/vue-markdown-loader) 是自己之前从 vuepress 中提取的 markdown-loader 部分代码改写出来的一个 webpack loader。它的主要功能是加载 markdown 文件,进行一些处理,如解析 emoji、代码高亮等,最后返回可以供 vue-loader 的内容。最近又进一步优化,让它可以返回 html 而被 html-loader 处理,或者直接返回一个包含 markdown 文件信息的对象。 56 | 57 | 配置如下: 58 | 59 | ```js 60 | build: { 61 | extend(config, ctx) { 62 | // frontmatter-markdown-loader 63 | config.module!.rules.push({ 64 | test: /\.md$/, 65 | include: path.resolve(__dirname, 'posts'), 66 | use: [ 67 | { 68 | loader: '@tianyong90/vue-markdown-loader', 69 | options: { 70 | mode: 'raw', // 这里表示 import md 文件后直接返回一个对象 71 | contentCssClass: 'markdown-body', 72 | markdown: { 73 | lineNumbers: true, // enable line numbers 74 | }, 75 | }, 76 | }, 77 | ], 78 | }) 79 | }, 80 | } 81 | ``` 82 | 83 | 4. 文章页的一些处理 84 | 85 | 有了前面的这些,就可以开始动手处理文章页了,这也是博客的关键部分。而其中最为重要的工作就是根据 url 中拼音化的文章标题正确加载 posts 目录中的 markdown 链接半渲染显示,这些基本都在 asyncData 方法中完成。 86 | 87 | ```ts 88 | async asyncData({ params }) { 89 | // 这里的 posts.json 是用脚本生成的保存 posts 目录中文章列表信息的 90 | // 相当于一个小的数据库 91 | const { default: posts } = await import('~/posts/posts.json') 92 | 93 | // 链接中拼音化的文件名 94 | const slugifiedFilename = params.slug 95 | 96 | const thePost: any = posts.find((item: any) => { 97 | return item.slugifiedFilename === slugifiedFilename 98 | }) 99 | 100 | // posts 目录中 markdown 实际文件名 101 | const filename = thePost.filename 102 | 103 | // 解析渲染都交给前面提到的 @tianyong90/vue-markdown-loader 完成 104 | // 这里的 html 就是渲染出来的 html,可以直接应用于 v-html 指令 105 | // attributes 则是 markdown 文件头部的 frontmatter 数据如标题、日期等 106 | const { html, attributes } = await import(`~/posts/${filename}.md`) 107 | 108 | return { 109 | ...attributes, 110 | html: html.replace(/src="\.\//g, `src="/_nuxt/posts/${filename}/`), // markdown 内容中图片地址引用替换 111 | } 112 | }, 113 | ``` 114 | 115 | 然后在模板中显示这些数据,其中 html 使用 v-html 指令就可以了。 116 | 117 | ```html 118 | 119 | 120 | 121 | 122 | {{ date }} 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | ``` 133 | 134 | 5. 布局、样式等细节 135 | 136 | 博客并不只是文字内容,因此还需要在布局、样式等方面下些功夫。因为自己设计水平实在有限,所以直接使用了 bootstrap 和 github-markdown-css,撸完文章列表页以及文章内容页就够用了,其它的页面看需要再加吧。 137 | 138 | 6. 生成、部署以及自动化 139 | 140 | 最后要生成静态页,而博客所使用的又是动态路由,就需要在 nuxt.config.js 中的 genarate 荐中进行配置。 141 | 142 | 如下,根据当前 posts 目录下的 markdown 文件名来确定该生成哪些页面。 143 | 144 | ```js 145 | generate: { 146 | routes: ['404'].concat(posts.map(post => `/posts/${post.slugifiedFilename}`)), 147 | }, 148 | ``` 149 | 150 | 执行 `yarn run generate` 后可以看到下面的结果,`dist` 目录里也出现了静态文件,剩下的就只是部署了。 151 | 152 |  153 | 154 | 对于部署,配置上 Circle CI,当推关新内容上 master 分支时由 CI 进行构建并部署到自己服务器简直不能更爽。 155 | 156 | 此外,为了省事,还写了几个脚本来创建新的 markdown 文件和相应的文件夹,虽然这也不是必须的,但使用脚本显然要比手动创建要省事得多。 157 | 158 | ## 总结 159 | 160 | Nuxt.js 确实是个好东西,写了近三年 Vue 了才开始盘它,确实是有点儿迟了。Nuxt 利用 SSR 等机制能很好地弥补 SPA 应用在 SEO 等方面的不足,其自带的生成静态站的功能也非常适合平时写一些博客之类的应用。 161 | 162 | 感谢 Hexo 陪伴多年(虽然期间也没用它写出什么东西来),但以后可能不会再用它了…… 😄 163 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 16 | 17 | 18 | 19 | 22 | {{ post.title }} 23 | 24 | 25 | 26 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 上一页 44 | 45 | 50 | 下一页 51 | 52 | 53 | 54 | 55 | 56 | 137 | 138 | 210 | -------------------------------------------------------------------------------- /stylelint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | plugins: ['stylelint-order'], 4 | extends: [ 5 | 'stylelint-config-standard', 6 | 'stylelint-config-standard-scss', 7 | 'stylelint-config-prettier', 8 | ], 9 | customSyntax: 'postcss-html', 10 | rules: { 11 | 'selector-pseudo-class-no-unknown': [ 12 | true, 13 | { 14 | ignorePseudoClasses: ['global'], 15 | }, 16 | ], 17 | 'selector-pseudo-element-no-unknown': [ 18 | true, 19 | { 20 | ignorePseudoElements: ['v-deep'], 21 | }, 22 | ], 23 | 'at-rule-no-unknown': [ 24 | true, 25 | { 26 | ignoreAtRules: [ 27 | 'function', 28 | 'if', 29 | 'each', 30 | 'include', 31 | 'mixin', 32 | 33 | // tailwindcss 34 | 'tailwind', 35 | 'apply', 36 | 'variants', 37 | 'responsive', 38 | 'screen', 39 | ], 40 | }, 41 | ], 42 | 'no-empty-source': null, 43 | 'named-grid-areas-no-invalid': null, 44 | 'unicode-bom': 'never', 45 | 'no-descending-specificity': null, 46 | 'font-family-no-missing-generic-family-keyword': null, 47 | 'declaration-colon-space-after': 'always-single-line', 48 | 'declaration-colon-space-before': 'never', 49 | 'declaration-block-trailing-semicolon': 'always', 50 | 'rule-empty-line-before': [ 51 | 'always', 52 | { 53 | ignore: ['after-comment', 'first-nested'], 54 | }, 55 | ], 56 | 'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }], 57 | 'order/order': [ 58 | [ 59 | 'dollar-variables', 60 | 'custom-properties', 61 | 'at-rules', 62 | 'declarations', 63 | { 64 | type: 'at-rule', 65 | name: 'supports', 66 | }, 67 | { 68 | type: 'at-rule', 69 | name: 'media', 70 | }, 71 | 'rules', 72 | ], 73 | { severity: 'warning' }, 74 | ], 75 | // Specify the alphabetical order of the attributes in the declaration block 76 | 'order/properties-order': [ 77 | 'position', 78 | 'top', 79 | 'right', 80 | 'bottom', 81 | 'left', 82 | 'z-index', 83 | 'display', 84 | 'float', 85 | 'width', 86 | 'height', 87 | 'max-width', 88 | 'max-height', 89 | 'min-width', 90 | 'min-height', 91 | 'padding', 92 | 'padding-top', 93 | 'padding-right', 94 | 'padding-bottom', 95 | 'padding-left', 96 | 'margin', 97 | 'margin-top', 98 | 'margin-right', 99 | 'margin-bottom', 100 | 'margin-left', 101 | 'margin-collapse', 102 | 'margin-top-collapse', 103 | 'margin-right-collapse', 104 | 'margin-bottom-collapse', 105 | 'margin-left-collapse', 106 | 'overflow', 107 | 'overflow-x', 108 | 'overflow-y', 109 | 'clip', 110 | 'clear', 111 | 'font', 112 | 'font-family', 113 | 'font-size', 114 | 'font-smoothing', 115 | 'osx-font-smoothing', 116 | 'font-style', 117 | 'font-weight', 118 | 'hyphens', 119 | 'src', 120 | 'line-height', 121 | 'letter-spacing', 122 | 'word-spacing', 123 | 'color', 124 | 'text-align', 125 | 'text-decoration', 126 | 'text-indent', 127 | 'text-overflow', 128 | 'text-rendering', 129 | 'text-size-adjust', 130 | 'text-shadow', 131 | 'text-transform', 132 | 'word-break', 133 | 'word-wrap', 134 | 'white-space', 135 | 'vertical-align', 136 | 'list-style', 137 | 'list-style-type', 138 | 'list-style-position', 139 | 'list-style-image', 140 | 'pointer-events', 141 | 'cursor', 142 | 'background', 143 | 'background-attachment', 144 | 'background-color', 145 | 'background-image', 146 | 'background-position', 147 | 'background-repeat', 148 | 'background-size', 149 | 'border', 150 | 'border-collapse', 151 | 'border-top', 152 | 'border-right', 153 | 'border-bottom', 154 | 'border-left', 155 | 'border-color', 156 | 'border-image', 157 | 'border-top-color', 158 | 'border-right-color', 159 | 'border-bottom-color', 160 | 'border-left-color', 161 | 'border-spacing', 162 | 'border-style', 163 | 'border-top-style', 164 | 'border-right-style', 165 | 'border-bottom-style', 166 | 'border-left-style', 167 | 'border-width', 168 | 'border-top-width', 169 | 'border-right-width', 170 | 'border-bottom-width', 171 | 'border-left-width', 172 | 'border-radius', 173 | 'border-top-right-radius', 174 | 'border-bottom-right-radius', 175 | 'border-bottom-left-radius', 176 | 'border-top-left-radius', 177 | 'border-radius-topright', 178 | 'border-radius-bottomright', 179 | 'border-radius-bottomleft', 180 | 'border-radius-topleft', 181 | 'content', 182 | 'quotes', 183 | 'outline', 184 | 'outline-offset', 185 | 'opacity', 186 | 'filter', 187 | 'visibility', 188 | 'size', 189 | 'zoom', 190 | 'transform', 191 | 'box-align', 192 | 'box-flex', 193 | 'box-orient', 194 | 'box-pack', 195 | 'box-shadow', 196 | 'box-sizing', 197 | 'table-layout', 198 | 'animation', 199 | 'animation-delay', 200 | 'animation-duration', 201 | 'animation-iteration-count', 202 | 'animation-name', 203 | 'animation-play-state', 204 | 'animation-timing-function', 205 | 'animation-fill-mode', 206 | 'transition', 207 | 'transition-delay', 208 | 'transition-duration', 209 | 'transition-property', 210 | 'transition-timing-function', 211 | 'background-clip', 212 | 'backface-visibility', 213 | 'resize', 214 | 'appearance', 215 | 'user-select', 216 | 'interpolation-mode', 217 | 'direction', 218 | 'marks', 219 | 'page', 220 | 'set-link-source', 221 | 'unicode-bidi', 222 | 'speak', 223 | ], 224 | }, 225 | ignoreFiles: [ 226 | '**/*.js', 227 | '**/*.jsx', 228 | '**/*.tsx', 229 | '**/*.ts', 230 | ], 231 | } 232 | -------------------------------------------------------------------------------- /components/Header.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 田写 10 | 11 | 12 | 13 | 19 | 20 | 24 | 31 | {{ post.title }} 32 | 33 | 34 | 35 | 36 | 37 | 44 | 49 | 个人简历 50 | 51 | GitHub 56 | 微博 61 | 62 | 63 | 68 | 69 | 73 | 80 | 84 | 个人简历 85 | 86 | GitHub 91 | 微博 96 | 97 | 98 | 99 | 100 | 101 | 179 | 180 | 256 | -------------------------------------------------------------------------------- /posts/在-Laravel-项目中使用-webpack-encore.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 在 Laravel 项目中使用 webpack-encore 3 | date: 2019-07-07 13:47:57 4 | top_img: ./encore.png 5 | tags: 6 | - webpack-encore 7 | - laravel-mix 8 | - laravel 9 | categories: 10 | - 前端 11 | --- 12 | 13 | 看过我之前写过的博客的应该知道我一直是 laravel-mix 的死忠粉,有好几篇文章都是关于它的。每每提到 laravel-mix 时更是不吝溢美之词。然而就在大概一个月前,我却决定不再使用它,而转投 webpack-encore 阵营。 14 | 15 | 至于为什么放弃 laravel-mix,主要是因为它的维护状况堪忧,不仅更新节奏缓慢,许多 Issue 久悬未决,更重要的是,作者似乎将很多 bug 完全寄希望于 webpack5,哪怕有热心人士 PR 了,也通常被关掉,然后回复说“兄 dei,这个坑等 webpack5 出来就好了,我之前试过没弄好,估计你这也填好坑,干脆安分点儿等 webpack5 吧”(不是原话,但差不多是这意思 :smile:)。但最终让我下定决心寻求替代方案的,则是这个 [Issue](https://github.com/JeffreyWay/laravel-mix/issues/1914),细翻源码,发现相关功能依赖的还是 extract-text-webpack-plugin,而这个包,早在 webpack4 发布不久就被宣布废弃了(现在去看它的官方仓库已经被设置为 archived),而作者似乎完全没有使用 mini-css-extract-plugin 的意思。 16 | 17 | 正所谓爱之深,责之切,在对 laravel-mix 表示失望之后,我翻出了自己 star 多时的另一包 webpack-encore,虽说很早就 star 了,但之前却没试用过它,可能也是因为对于 laravel-mix 的偏爱,然而这次,不试便罢,试完之后大有相见恨晚之意。 18 | 19 | [webpack-encore](https://symfony.com/doc/current/frontend/encore/simple-example.html) 是 Symfony 官方的前端集成构建工具,同样是基于 webpack,但它的 API 设计得更为友好,而且文档更完善,当然更关键的一点是,坑更少啊……从开始读它的文档,倒把手里一个项目从 laravel-mix 迁移到 webpack-encore,只用了几个小时,并且期间相当顺利。而我迁移的这个项目,是一个 Laravel 项目,所以下面就分享下,如果在 Laravel 项目中使用 webpack-encore 替代 laravel-mix。 20 | 21 | ## 安装依赖 22 | 23 | 首先当然是安装依赖 24 | 25 | ```bash 26 | yarn add -D @symfony/webpack-encore 27 | ``` 28 | 29 | 需要注意的是,webpack-encore 没有像 laravel-mix 那样在自己内部依赖 vue-tempplate-compiler 之类的包,所以如果自己项目里用动了这些,需要自己在项目里手动安装好。 30 | 31 | ## 配置 webpack 32 | 33 | 在项目根目录下新建一个 webpack.config.js 文件并在其中配置 webpack-encore 功能(实际上它最终也是一个标准的 webpack 配置文件),以最基本的玩法为例。 34 | 35 | ```js 36 | const Encore = require('@symfony/webpack-encore') 37 | 38 | Encore 39 | // directory where compiled assets will be stored 40 | .setOutputPath('public/js/') 41 | // public path used by the web server to access the output path 42 | .setPublicPath('/js') 43 | // only needed for CDN's or sub-directory deploy 44 | //.setManifestKeyPrefix('build/') 45 | 46 | /* 47 | * ENTRY CONFIG 48 | * 49 | * Add 1 entry for each "page" of your app 50 | * (including one that's included on every page - e.g. "app") 51 | * 52 | * Each entry will result in one JavaScript file (e.g. app.js) 53 | * and one CSS file (e.g. app.css) if you JavaScript imports CSS. 54 | */.addEntry('app', './resources/js/app.js') 55 | 56 | // will require an extra script tag for runtime.js 57 | // but, you probably want this, unless you're building a single-page app 58 | .enableSingleRuntimeChunk() 59 | 60 | .cleanupOutputBeforeBuild().enableSourceMaps(!Encore.isProduction()) 61 | // enables hashed filenames (e.g. app.abc123.css) 62 | .enableVersioning(Encore.isProduction()) 63 | 64 | .enableVueLoader() 65 | .enableSassLoader(options => { 66 | options.implementation = require('sass') 67 | }) 68 | 69 | // fetch the config, then modify it! 70 | const config = Encore.getWebpackConfig() 71 | 72 | // export the final config 73 | module.exports = config 74 | ``` 75 | 76 | 77 | ## 新增 php helper 函数 78 | 79 | Laravel 自带了一个 mix() 函数用于引用 mix 编译的资源,与之类似,syfony 也有这样的函数,而且更为方便。为此你需要在 Laravel 项目中自行实现这两方法,下面是我参考 symfony 里相关源码改写的,可能逻辑上并不算完善,但以自己一个多月的使用情况来看,它们表现良好。 80 | 81 | ```php 82 | use Illuminate\Support\HtmlString; 83 | 84 | /** 85 | * @param string $entryName 86 | * @return HtmlString 87 | */ 88 | function encore_entry_link_tags(string $entryName): HtmlString 89 | { 90 | $entryPointsFile = public_path('js/entrypoints.json'); 91 | 92 | $jsonResult = json_decode(file_get_contents($entryPointsFile), true); 93 | 94 | if (!array_key_exists('css', $jsonResult['entrypoints'][$entryName])) { 95 | return null; 96 | } 97 | 98 | $tags = array_map(function ($item) { 99 | return ''; 100 | }, $jsonResult['entrypoints'][$entryName]['css']); 101 | 102 | return new HtmlString(implode('', $tags)); 103 | } 104 | 105 | /** 106 | * @param string $entryName 107 | * @return HtmlString 108 | */ 109 | function encore_entry_script_tags(string $entryName): HtmlString 110 | { 111 | $entryPointsFile = public_path('js/entrypoints.json'); 112 | 113 | $jsonResult = json_decode(file_get_contents($entryPointsFile), true); 114 | 115 | if (!array_key_exists('js', $jsonResult['entrypoints'][$entryName])) { 116 | return null; 117 | } 118 | 119 | $tags = array_map(function ($item) { 120 | return ''; 121 | }, $jsonResult['entrypoints'][$entryName]['js']); 122 | 123 | return new HtmlString(implode('', $tags)); 124 | } 125 | ``` 126 | 127 | ## 使用 `encore_entry_link_tags` 和 `encore_entry_script_tags` 引用编译的前端资源 128 | 129 | 在模板里使用前面添加的 helper 函数引用资源,你会发现它比 Laravel 自带的 mix() 函数更方便,只需要一个函数,就可以自动引入 vendor.js 和 app.js 了。 130 | 131 | ```php 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | {{ config('app.name') }} 143 | 144 | 145 | {{ encore_entry_link_tags('app') }} 146 | 147 | 148 | 149 | 150 | 151 | {{ encore_entry_script_tags('app') }} 152 | 153 | 154 | ``` 155 | 156 | ## 修改 package.json 中的脚本(scripts) 157 | 158 | 因为 laravel 项目默认 package.json 中 develop 等相关的脚本都是使用 laravel-mix 的,为了方便日常开发,现在要对它们进行一些调整,改用 webpack-cocore。调整后大致如下,你也可以根据自己实际应用情况进行其它调整 159 | 160 | ```json 161 | "scripts": { 162 | "dev": "npm run development", 163 | "development": "cross-env NODE_ENV=development encore dev", 164 | "watch": "npm run development -- --watch", 165 | "watch-poll": "npm run watch -- --watch-poll", 166 | "hot": "encore dev-server --port=9001 --hot", 167 | "prod": "npm run production", 168 | "production": "cross-env NODE_ENV=production encore production" 169 | }, 170 | ``` 171 | 172 | ## 运行脚本,愉快撸 BUG 173 | 174 | 做完前面的这些步骤之后,在终端执行 `yarn run hot`,浏览器中输入项目绑定的域名(如 app.test),就可以体验方便高效的 HMR 开发了。 175 | 176 | ## 后记 177 | 178 | 使用 webpack-encore 已经快两个月了,这期间总体说来相当顺利,小坑虽然有,但没什么大坑。去 github 上提 issue,维护成员基本上都很友善耐心,几个小时就会有回复。这种态度也让我对它更加放心了,相信它会折腾得越来越好。虽然 webpack-encore 是作为 Symfony 默认集成工具来设计的,但这并不妨碍它在 Laravel 中发挥强大威力。 179 | 180 | 相比于 laravel-mix,encore 的 API 以及一些默认配置方面考虑得更为科学和全面,想要配置 vue-loader 或者 ts-loader 之类的,只需要调用相应的方法。另外还有点让我先惊讶的是,他们竟然对 `watchOptions.ignored` 的默认值也考虑到了,默认忽略 /node_modules/,降低 CPU 占用。当然,更为重要的是,mix4 里因为一些 bug 而无法使用的功能,在 encore 里却正常,如 dynamic import。 181 | 182 | 总之,如果你已经发现了 laravel-mix 的种种不足但又苦于没更好选择的话,不妨试试 webpack-encore,相信你会对它爱不释手。 183 | --------------------------------------------------------------------------------
20 | {{ item.description }} 21 |
{{ checkedUser }}