本文不是什么技术性介绍文章,准确地说算是自己的成长记录吧。刚参加工作时,组里使用的脚手架是由 leader 使用 webpack, gulp 搭建的多页面应用脚手架 fex。当时只需知道怎么使用就行了,不过为了能更好地工作,对 fex 怎么构建一直很好奇,也一直关注相关的技术栈。经过一年多磨练后,对 fex 怎么搭建的有了个大概地认识。常言道:"没有对比就没有伤害"。

在使用 vue-cli 构建第一个 vue 项目后,对脚手架构建有了个全新的认识。发现 fex 存在很多不足:

  • 在打包时,只对 JavaScript 和 CSS 脚本文件进行打包压缩处理。不能对资源文件(如 img,字体等)进行依赖处理。导致在打包时:

    • 不能按需打包(即实际用到资源,才将其进行打包)
    • 不能进行 MD5 处理
    • 不能输出压缩版的 html
  • 手动注入 JavaScript 和 CSS 脚本文件,如果需要做优化,会很不方便,特别在多页面情况下。
  • dev 与 build 使用不同的技术方案,增加定制的成本。
  • 基于 nodemon 对开发目录进行 watch,当执行修改操作时,会重启整个服务。会存在重启服务耗时比较长的情况,导致刷新页面出现空页面的情况,开发体验不是很好。
  • 缺少 code-splitting、HMR、端口检测、Babel 等功能。

当然,fex 也有自己的优点。基于自建服务提供前后端复用模板功能。前端后端使用相同的模板语言,前端拼接的模板可以直接输出给后端使用。

第二年年初,组里项目不是太多,刚好有时间折腾一下,于是决定构建一个全新的脚手架 fes。为了尝试一些新东西,在技术栈上,都使用了当时最新的技术框架 webpack4、koa2、babel6 来搭建。为了了解 webpack 如何工作,对 webpack 就做了 8 次调试,才稍微对 webpack 整个架构有个初步认识。

singsong: 在真正去了解 webpack 时,才知道它有多复杂。当然也参考了网上一些大神分享关于 webpack 源码分析的文章。反正整个过程还是挺熬心的?

同时,还对 koa2、babel6 做了相关的研究。附一张 koa2 分析图吧?:

为了提高 fes 开发体验,除了继承 fex 的模板复用功能外,还集成了 vue-cli 中不错的功能。

  • 兼容 macOS、windows、Linux 等操作系统,同时兼容主流浏览器及 IE 低版本。
  • ES6、SASS
  • js-code-splitting、css-code-splitting
  • 多页面开发环境
  • proxy
  • css autoprefixer
  • css/svg sprite
  • 支持更灵活定制,如是否自动打开浏览器、热加载等配置。
  • 自动监听 port,如果被占用,提示性切换。
  • 打包优化分析
  • 模板输出(便于后端复用模板)
  • 基于mockjs 模拟 api。

在搭建 fes 过程中,自己对前端代码规范化的重要性有了自己的一些思考:

前端开发的规范如 JavaScript 弱类型特性一样,没有统一规范。每个人都有自己的一套编码风格。这对团队来说并不是一件什么好事。就拿我们团队来说吧。大多数的项目(前端)都是由个人来维护,很少有团队合作的项目。因为每个人编码风格不同,导致下个接手维护的人需要重新习惯这种编码风格。这就存在一定的学习成本,而且效率不高。可能原维护人只需花几分钟解决的事,接手人需要花几个小时,甚至更多的时间和精力。对团队合作项目来说,统一的编码风格显得更为重要。因为不同的编码风格会让团体开发进度大打折扣,维护起来也很费力。另外,开发人员会对彼此编码习惯存在不同程度的排斥现象。

项目规范化的辅助性工具:

  • eslint:规范 js 代码
  • stylelint:规范 css 代码
  • editorconfig:规范 IDE
  • husky 和 lint-staged:在 pre-commit 时 eslint、stylelint,确保风格一致、高质量的代码输出。

规范化的好处:

  • 规范化团队的编码风格,便于团队内项目的维护。
  • 规范化可让开发规避一些常见的错误。如未使用的变量;文件命名错误,未能成功导入等。
  • 规范化对新人有很好的指导作用,好的开始很重要。因为这些规范都是行业内一些最佳实践,可让新人成长得更加专业化。

为了促进团队的代码规范化,自己也将 eslint、stylelint、prettier、husky、lint-staged 集成到 fes 中。

当然整个 fes 搭建过程中也并不是一帆风顺的,途中也遇见一些坑:

  • koa2 与 html-webpack-plugin

    在开发模式下,fes 是基于 html-webpack-plugin 插件自动生成 HTML 文件,而 html-webpack-plugin 插件合成的 html 缓存于内存中,为了配合 koa2 输出合成的 html 文件,需要将 html 文件写入磁盘中。而要将 html-webpack-plugin 合成的 html 文件输出到磁盘中,需要借助 html-webpack-harddisk-plugin 插件。html-webpack-harddisk-plugin 是个基于 html-webpack-plugin 的插件。

  • html 中 img 的解析

    向来 webpack 对 html 的解析不是很友好。虽然 webpack 提供了 html-loader 来解析 html 中的 img。但 html-loader 是基于字符正则匹配来解析,即解析的是 html。但 fes 使用的是模板文件,这就需要对应模板 loader 来将其转换为 html。而 webpack 对 loader 的实现制定了相关的规范,为了提高编译性能,loader 一般返回的是一个 runtime 字符串,而不是最终编译后的输出。这样不仅有效地避免每次重新生成,也方便共享。所以为了能让 html-loader 解析模板文件,需要对模板 loader 做些定制,将其输出由 runtime 变为最终输出编译结果。

  • 对 twig 模板 include 文件修改,重编译不生效

    开启 twig.cache(false),也不能解决这个问题。经查阅 twig.js 源码后,需要通过twig.extend扩展,对缓存对象进行初始化,来禁掉缓存。

    // 去掉缓存
    Twig.extend(T => {if (T.Templates && T.Templates.registry) {T.Templates.registry = {};}
    });
    复制代码
  • postcss-sprites 不支持 webpack 的 alias

    因为 postcss-sprites 是 postcss 的插件,独立于 webpack。要让 postcss-sprites 支持 alias,只能扩展 postcss-sprites 让其支持与 webpack 一样的 alias 配置项。需要在遍历样式节点时,根据 alias 配置项替换,换成真实数据。

    const replaceAlias = image => {const {alias} = opts;let {url, originalUrl} = image;const tempUrl = url;if (/^~/.test(url)) {Object.keys(alias).forEach(item => {url = url.replace(RegExp('^~' + item), alias[item]);if (url !== tempUrl) {originalUrl = path.relative(path.parse(styleFilePath).dir, url);url = originalUrl;// 替换源码rule.replaceValues(tempUrl, {fast: tempUrl}, s => url);}});}image.url = url;image.originalUrl = originalUrl;return image;
    };
    复制代码
  • 模板复用

    fes 是基于 webpack-html-plugin 插件自动生成合成的 html 文件。但为了提供工作效率,业务中存在对模板复用的需求,所以需要重新定制输出。

    思路:通过 webpack-manifest-plugin 输出资源清单 manifest,再根据 manifest 将资源注入到模板中。另外,为了方便替换 html 中的图片资源,还需要将 html-loader 解析结果作为依赖替换。

    {"commonScripts": {"common.js": "/static/js/common.486cb059.chunk.js","vendors.js": "/static/js/vendors.11aa87af.chunk.js"},"commonCss": {"common.css": "/static/media/common.6094b30a.css"},"scriptFiles": {"index.js": "/static/js/index.bc043de1.js","home.js": "/static/js/home.d8768213.js","about.js": "/static/js/about.a3e6551a.js"},"cssFiles": {},"assets": {"static/media/logo.jpg": "/static/media/logo.da5595d8.jpg","static/media/ant2.png": "/static/media/ant2.89ca7b1b.png","static/media/ant1.png": "/static/media/ant1.ed485ba9.png",},"htmlFiles": {"about.html": "/about.html","home.html": "/home.html","index.html": "/index.html"}
    }
    复制代码

大概经历一个半月的时间,fes 也如期而至。于是就在组里推广使用,自己也使用开发了几个项目。与 fex 相比,fes 在开发效率、体验上都得到很大的提升。但同时也暴露一些问题,其中最头疼的问题是:由于没有将核心代码提取作为依赖包。导致在使用过程中升级维护不是很方便。一般若发现问题都是现场解决,然后再同步到代码库中。但这样不能很好地将代码同步其他已使用项目中。

在经过半年的沉淀后,决定对 fes 进行重构。并整理了一些优化点:

  • 优化热加载。
  • 支持模板语言 loader 的配置。
  • 支持 css 预处理器 loader 的配置。
  • 引入 common.js,方便添加公用代码,避免每个 js 文件重复引用。
  • 优化某些页面没有对应的 js 文件。
  • 去掉 jquery 中为默认内置。
  • 支持路由配置。
  • 支持多级目录结构。
  • 将 media.json 放入 gitignore。
  • sprite 合成会引起一次编译,大多数情况这次编译是多余的。
  • 可以将 start、tmpl、preview 脚本进行优化,提出共有逻辑,增加复用性,和可维护性。
  • 支持 CSS-Modules。
  • 支持 typeScript。
  • 优化编译,打包时间。
  • 增加 service worker。
  • babel6 升级到 babel7。

不过在重构过程中,在是否将 Babel 内置于 fes 中有了一些新的思考 ?。

在搭建 fes 初版时,只要觉得功能不错都会集成于 fes 中。但并不是所有的项目都需要所有功能,而且这样会导致 fes 变得臃肿。也就是说有些可选功能,没必要作为内置功能。如 babel、typeScript、stylelint、eslint、precommit 等。其实 fes 只需内置基础架构即可,其他可选功能可以通过配置来定制。这样不仅可让 fes 变得灵活轻巧,而且也方便扩展。

为了将 fes 的核心代码提取作为依赖包,参考了 create-react-app 构建。毕竟 create-react-app 是个明星项目,技术也相对稳定成熟。加上之前也使用 create-react-app 构建几个 react 项目,对其也算有点了解,不过只停留在使用上。如果要重构 fes 还需要对 create-react-app 源码深入研究一番。

整个 fes 的构建完全基于 create-react-app。代码结构也由两个 packages 组成:create-fes 和 fes-scripts。但对于如何维护这两个 packages 是一个很棘手的问题。如果独立分开管理,开发起来不是很方便,后期维护成本也高(如版本号维护)。于是查看了 create-react-app 源码,发现在其源码中有一个 lerna.json 文件。好奇这个文件是做什么的,就了解一番。经查阅了解到 Lerna 可以用来管理项目中多个 packages。这正是自己所需要的,为此自己也专门写了一篇 Lerna 文章:monorepos by lerna。

在这次重构中自己也做了一些优化,让 fes 的体验得到很大地提升。

  • 动态响应mock api(模拟服务请求接口)

    在 fes 初版时,对 mock api 的修改,需要重启服务才能生效。这样体验在开发中不是很友好的。于是就开始折腾,有木有什么方法能让 mock api 的修改不用重启就能生效。一开始想到的解决方案是基于 nodemon,但是这样只要对 mock api 文件做修改,就会重新服务。如果频繁地修改,会不停地重启服务,影响到正常的开发服务,不是很理想。那另开一个服务来专门服务于 mock api,再基于 nodemon 监听变化,这样就不会影响正常的开发服务。但这样整个架构就变得有点重了。看来基于nodemon的思路是走不通了,只能换一个思路?。对请求响应着手,在响应请求时,去掉缓存,确保每次响应都是最新的数据,这样问题不就迎刃而解么?。但需要过滤掉静态资源的请求,不然会影响页面的响应时间。

    // mockApi 中间件
    const mockApi = async (ctx, next) => {if (!ctx.path.includes('/static')) { // 过滤掉静态资源// avoid loading static resources with delayconst mockContext = {mock(path) {const url = join(paths.appApis, path);delete require.cache[url];// 删除缓存return Mock.mock(require(url)); // eslint-disable-line},};delete require.cache[join(paths.appApis, 'index.js')];// 删除缓存const api = require(paths.appApis); // eslint-disable-lineconst mockData = api.call(mockContext);const responseBody = mockData[ctx.url];if (responseBody) {ctx.body = responseBody;if (typeof responseBody === 'function') {try {const responseBodyFromFun = responseBody.call(mockContext);ctx.body = responseBodyFromFun.data;if (typeof responseBodyFromFun.others === 'function') {responseBodyFromFun.others(ctx);}} catch (error) {console.log(`${chalk.bold.red('Error: ')}`, error);}console.log(`${chalk.black.bgYellow('MOCK-APIs')}   ${chalk.bold.green(ctx.method)}  ${chalk.gray('--->')}  ${chalk.dim(ctx.url)}`);}}}await next();
    };
    复制代码
  • css 热加载

    之前 webpack 支持 css 热加载,一直由 sytle-loader 来完成。而 style-loader 是基于 js 将待更新的 css 注入到 DOM 中,这样会导致 FOUC(flash of unstyled content) 问题。为了避免 FOUC,可以使用 mini-css-extract-plugin,尴尬的是 mini-css-extract-plugin 不支持 hmr。不过可以配合 css-hot-loader 让其支持 hmr。而 css-hot-loader 工作原理是将 mini-css-extract-plugin 提取的 css 中注入热加载相关代码来实现热更新的。

    好消息是在重构过程中 mini-css-extract-plugin 在 0.0.6 版本开始支持 hmr???。

    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    module.exports = {plugins: [new MiniCssExtractPlugin({// Options similar to the same options in webpackOptions.output// both options are optionalfilename: '[name].css',chunkFilename: '[id].css',}),],module: {rules: [{test: /\.css$/,use: [{loader: MiniCssExtractPlugin.loader,options: {// only enable hot in developmenthmr: process.env.NODE_ENV === 'development',// if hmr does not work, this is a forceful method.reloadAll: true,},},'css-loader',],},],},
    };
    复制代码
  • [template]-loader

    为优化,webpack loader 在输出时,一般都是一个 js runtime 字符串。而 js runtimehtml-loader不是很友好,特别对一些结构完全与html结构不相似的 template engine,如pug。如果不使用html-loader可以忽略。但在实际开发过程,在 html 中直接插入图片或其他资源还是个很常用的需求。

    要解决这个问题,需要对 loader 做一些定制。确保 loader 的输出是编译好的 html,这样对下游html-loader 处理就很友好了。同时,这样也方便对 mock 数据的处理。

    另外,在编写 template loader 时,需要确保支持视图模板文件 base 目录的指定,即支持绝对路径引用。因为后端常使用绝对引用方式。为了方便前后端模板复用,最好与后端的引用方式保持一致。

  • 去掉 postcss-modules,使用 css-loader 的 css-modules

    之前使用 postcss-moudles,主要解决将经过css-modules 编译后的类名对象与模板变量数据合并作为模板渲染数据。这种模式对当前的开发场景来说不如将在js中使用灵活。

    尝试:将 css-modules 与模板数据结合在一起也算一种新尝试。可以制定一定规范,只要所有模板都遵循这一套规范也是一种不错的开发方式。

  • 将 runtime chunk 合并到 vendors 中。

    在对打包进行优化时,为了使用浏览器缓存,使用 runtimeChunk: 'single' 提出 runtime chunk。同时也使用splitChunks生成一个 commons chunk 和 vendor chunk。因为 runtime chunk 和 vendor chunk 属于不常改变的代码,可以将两者打包到一个 chunk 中。 查阅文档,也没有提供相关的方法。于是就去了 webpack 的 gitter 和 Stack Overflow 提问。但是没有人鸟我 ?(也许自己的英语太差,没有表述清楚?或这个问题 too easy)。

    module.exports = {
    entry: {pageA: "./pageA",pageB: "./pageB",pageC: "./pageC"
    },
    mode: 'development',optimization: {runtimeChunk: 'single',splitChunks: {cacheGroups: {commons: {chunks: "initial",minChunks: 2,maxInitialRequests: 5, minSize: 0},vendor: {test: /node_modules/,chunks: "all",name: "vendor",priority: 10,enforce: true}}}
    },
    output: {path: path.join(__dirname, "dist"),filename: "[name].js"}
    };
    复制代码

    后续,在帮同事将老项目迁移到 fes 中时,因为存在历史包袱,需要重新定制打包方式。在此过程中无意中发现一种解决方案:

    module.exports = {entry: {pageA: "./pageA",pageB: "./pageB",pageC: "./pageC"},mode: 'development',optimization: {runtimeChunk: { name: 'vendor'},splitChunks: {cacheGroups: {commons: {chunks: "initial",minChunks: 2,maxInitialRequests: 5,minSize: 0},vendor: {test: /node_modules/,chunks: "all",name: "vendor",priority: 10,enforce: true}}}},output: {path: path.join(__dirname, "dist"),filename: "[name].js"}};
    复制代码

    runtimeChunk: { name: 'vendor'}name 设置与 cacheGroups['vendor']['name']: "vendor"相同即可,就这么简单?。

  • focus,提高编译速度

    在进行项目迭代时,有时需要新增页面。如果项目已存在页面很多。这样每次编译都需要重新编译一遍,会导致整个编译速度变得很慢。而在开发时,其实只需关注新增的页面,其他的页面是没必要编译的。所有就新增这个 focus 功能,来提高编译速度。只需指定新增的页面的文件名即可,同时支持多个文件名的指定。

  • 减少读磁盘操作

    在 fes 初版的开发模式下,fes 是基于 html-webpack-plugin 插件自动生成 HTML 文件并缓存在内存中,为了配合 koa2 输出合成的 html 文件,需要借助 html-webpack-harddisk-plugin 插件将 html 文件写入磁盘中。

    为了减少读磁盘操作,基于 global 将 html 数据存储在 global.__fes_bind_views_data__ 变量中。另外,还将 html-loader 解析的结果由之前的 media.json 文件转由 shareData 变量代替。

  • foolMode

    该模式是针对一些简单项目而新增的功能,在打包时会将所有的文件打包到一个文件,即最后输出结果一个js、一个css。

  • debug

    该模式主要用于帮助调试打包代码。在开启 debug 模式,build 出的代码不会被压缩,同时生成source map。方便开发调试。

到此 fes 构建之旅也告一段落,接下就不断地完善。

这就是自己构建 fes 的过程。构建过程中存在很多挑战,特别遇见一个花很多天也不能解决的问题,对自己的积极性、自信心打击还是挺大的。途中真有想放弃的念头,欣慰的是自己最终还是坚持了下来?。虽然在搭建完后,成就感并不如想象中那么大。因为感觉就那么一回事,觉得任何人只要花点时间也能完成。不过整个过程下来自己也收获不少。无论是在技术知识上、或对问题处理上等都得到了很大地提升。同时对自己也有了一个新的认识。人嘛就得对自己狠点,不然你真不知道自己有多大的潜能?。

好了,文章到此就结束。重构的 fes 已放置 github: create-fes。毕竟团队业务场景存在局限性,加上个人能力有限。为了让 fes 变得更好,最好的选择就是将其放置github。

喜欢的小伙伴请随意 github: create-fes 拉代码体验。欢迎使用?欢迎使用?欢迎使用?

转载于:https://juejin.im/post/5cfe84dae51d45106172109e

为什么我要构建这个脚手架相关推荐

  1. React+Webpack+Eslint+Babel构建React脚手架

    React+webpack+Eslint+Babel构建React脚手架 参考网上文章,说的不是很全,想自己写一篇来巩固知识点,脚手架源码参考阮一峰老师的Github 所用技术栈 React Babe ...

  2. 使用 .NET CLI 构建项目脚手架

    前言 在微服务场景中,开发人员分配到不同的小组,系统会拆分为很多个微服务,有一点是,每个项目都需要单元测试,接口文档,WebAPI接口等,创建新项目这些都是重复的工作,而且还要保证各个项目结构的大体一 ...

  3. Java中如何快速构建项目脚手架

    文章目录 1 前言 2 微服务项目准备 3 脚手架构建 3.1 项目正常启动 && 测试用例正常 3.2 在项目的根pom中加入以下maven插件配置 3.3 执行archetype插 ...

  4. 【开源项目】SpringCloud 快速构建项目脚手架工程(持续更新)

    一.项目地址 https://gitee.com/smile-coding/springcloud-quick-start 二.项目说明 本项目旨在提供一个快速构建微服务的脚手架工程,不掺杂任何的复杂 ...

  5. IDEA构建VUE脚手架

    一. 安装Node.js 1.官网下载nodeJS 2.下载完按提示一步一步安装 3.安装成功后如下图 4.配置环境变量: 4.1在安装目录下创建如下2 个目录文件 4.2.创建完,在cmd窗口中输入 ...

  6. (转)用webpack4从零开始构建react脚手架

    使用脚手架 git clone git@github.com:xiehaitao0229/react-wepack4-xht.git cd react-webpack4-xht `npm run de ...

  7. Webpack 入门;构建项目,脚手架

    1. 选择一个文件夹: D:/usr/webpack/demo01,并且在该目录下创建一个package.json 空白文件 demo |- package.json 2. 打开vscode: 3. ...

  8. vue-cli脚手架构建项目注意事项(填坑)

    在添加百度富文本Umeditor时,提示了很多错误,比如()前后要有空格.关键字UM未定义,查询后发现是构建VUE脚手架时候出的问题: 应该按照以下规则创建 一.ESLint的作用是检查代码错误和统一 ...

  9. [ 云计算 华为云 ] 华为云开天 aPaaS:构建高效的企业数字化平台(下)

    文章目录 前言 四.华为云开天aPaaS 核心功能 4.1 业务模型管理 4.2 连接器 4.2.1 连接器的种类 4.2.1.1 公共连接器 4.2.1.2 私有连接器 4.2.2连接器的开发步骤 ...

最新文章

  1. 一行代码,得到最强时序基线!
  2. slicer安装_3D Slicer教程【软件安装及设置】
  3. MySQL配置文件优化
  4. 从Flash到Silverlight进阶教程-用代码来创建动画
  5. 台式计算机计量单位,计算机的计量单位以及常见的数据类型
  6. mysql 5.5主从同步_MySQL5.5+配置主从同步并结合ThinkPHP5设置分布式数据库
  7. CentOS 7 Docker部署phpMyAdmin网站
  8. Keil 5安装教程
  9. 数据结构课程设计——学生成绩查询与分析系统(简单详细版,含讲解)
  10. 鸿蒙形容欣欣向荣发展,比喻事业蓬勃发展繁荣兴旺的成语蒸蒸日上
  11. win10 软路由_N合1服务器!NAS、软路由、高清盒子、Web一个都不能少!
  12. maven 实战 (许晓斌)
  13. 一文搞懂 | Linux 同步管理(上)
  14. 1 -- > PCI / PCIe 配置空间详解
  15. Spring Boot 项目参数校验的常见使用场景
  16. python中fabs函数_Python fabs() 函数 - Python 教程 - 自强学堂
  17. esxi中利用ovf模板迁虚拟机
  18. 网格交易法以及在数字货币中基于Python的量化实现
  19. jmeter模拟需验签的请求时注意参数中含有特殊字符要特别处理
  20. Interop统计WORD字数

热门文章

  1. mysql中的基本数据类型_mysql基本数据类型
  2. 训练自己的数据_PyTorch版CenterNet训练自己的数据集
  3. 一元线性回归决定系数_回归分析|笔记整理(1)——引入,一元线性回归(上)...
  4. java对象布局查看工具_Java 查看对象布局工具 - Java Object Layout
  5. MPI 环境搭建问题-运行程序闪退
  6. cuDNN编写卷积实例
  7. Solidity编程 二 之Solidity安装
  8. mvc:annotation-driven/浅析
  9. jQuery操作radio、checkbox、select总结
  10. 5.26在网上看到的方法,实现图形缩放、对齐、图形修改后进行dirty check。(未实验过)...