组内已经有了很是完善以及流畅的开发,发布流程,平时只须要默默地搬属于本身的那块砖就行了,可是每当社区出了新的技术,想尝试的时候老是欠缺一个“起手式”,能够快速将新的技术给集成到本身的脚手架,或者说工做流中,基于这个目的,想到就开始作了javascript
脚手架对团队的好处不言而喻,能够经过命令行的方式去快速生成种子文件,开发以及输出构建后的代码,平时咱们只须要开发,而不用跟复杂的编译过程,搭建服务等流程打交道,另外,还能够将咱们须要的node
模块安装到脚手架内,之后咱们只负责开发而不须要安装庞大的node_module
了,保持目录的干净,甚至脚手架还能够跟后续的持续集成相结合,提供更强大的功能css
从零开始搭建脚手架须要必定的前端工程化知识,推荐看webpack指引,里面涉及了大量前端工程化须要作的事情,事实上我也是从这里一步一步地往上搭上去的,并最终开发完脚手架qd-cli(音译:前端-cli,语文很差- -!),开发脚手架本质上仍是写webpack
,用webpack
搭建工做流,并最终可使用commander将其封装成命令行工具,这篇文章对commander
介绍得很详细了,再也不重复:基于node.js的脚手架工具开发经历html
本文从如下三个方面作介绍,搭建:如何一步步开发qd-cli(包含了我对前端工程化的了解)
,qd-cli的安装,使用,特性
,搭建过程当中遇到的一些坑
前端
先从简单地作起,再慢慢地往上堆砌,所以,目前考虑的是只支持移动端项目
,以及vue技术栈
vue
技术方案:工做流的编写毫无悬念地选择了webpack,现下最热门的前端打包工具,webpack首要解决了前端模块化的难题,开箱即用,原生支持es module
,这里选择最新的webpack4
,另外一方面,将工做流集成成cli
使用commanderjava
主要考虑如下三个方面:node
在开发环境须要有服务器去启动并自动刷新咱们的应用,有时甚至指望能够设置代理,便于先后端联调,可使用webpack-dev-server,配置很简单react
// webpack.config.js module.exports = { // ... + devServer: { + ... + contentBase: cwd('dist'), + proxy: { ... } + } } 复制代码
在更改代码后无需手动刷新浏览器便可预览效果,快速便捷,即便js的热重载有点坑,有时须要手动去刷新,但整体仍是利大于弊的jquery
const webpack = require('webpack'); module.exports = { devServer: { ... + hot: true, contentBase: cwd('dist'), proxy: { ... } }, plugins: [ + new webpack.NamedModulesPlugin(), + new webpack.HotModuleReplacementPlugin() ] } 复制代码
webpack打包后的代码报错后不利于咱们去定位错误位置,soucemap
能够帮咱们准肯定位到源码的出错位置webpack
const webpack = require('webpack'); module.exports = { + devtool: 'inline-source-map' devServer: { hot: true, contentBase: cwd('dist'), proxy: { ... } }, plugins: [ new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin() ] } 复制代码
生产环境配一个最简单的source-map
就能够了,由于复杂一点的source-map
通常体积都很大
生成环境须要尽量地优化代码的体积,webpack为咱们提供了完整的方案,只需一点点的配置
代码分割是一件颇有必要的操做,在多页应用中,A,B,C页面可能同时依赖了大量的第三方库,将公共库抽取出来利于浏览器作缓存,并能有效减小A,B,C页面的体积
单页应用也应作代码分割,将第三方库抽取出来,一方面,咱们平时须要不断迭代的部分通常都是业务代码,第三方库的代码是不会有变更的,这样的抽取一样利于浏览器作缓存,另外一方面,js是单线程的,包的体积太大意味着下载变慢,致使js线程被挂起
module.exports = { ... optimization: { splitChunks: { cacheGroups: { // 抽取node_modules中的第三方库 vendors: { test: /[\\/]node_modules[\\/]/, name: "vendors", chunks: "all" }, commons: { name: "commons", chunks: "initial", minChunks: 2 } } } } } 复制代码
摇树利用了export,import
的静态特性,将代码中的无用代码给删掉,好比在代码中:
import { forEach } from 'lodash-es' 复制代码
在最后的打包过程,webpack只会将lodash-es
中的forEach
方法打包进来,其余无用的代码不会打包进来,摇树(tree shaking)
在webpack中的配置很是简单,以下:
module.exports = { mode: 'production' } 复制代码
在babel配置里面须要:
module.exports = { presets: [ [ 'env', // 启动tree shaking { modules: false } ], 'stage-2' ] ... } 复制代码
补充:摇树的概念大概指的是,将咱们的代码比喻成一棵树,将无用的代码(枯黄的叶子)给摇下来,这里踩了一个坑,后面补充
为了提高首屏时间,不少代码均可以延迟加载,在webpack体系打包的代码中,使用懒加载很是方便
// 方法1 import('./someLazyloadCode').then(_ => {...}) // 方法2, 如下使用方式称为魔法注释,能够将最后生成的文件命名为lazyload,利于咱们去分析打包后的代码 import(/* webpackChunkName: "lazyload" */ './someLazyloadCode').then(_ => {...}) 复制代码
在vue
中使用也很方便,能够参考Lazy Loading in Vue using Webpack's Code Splitting
注意,使用懒加载须要添加promise垫片,由于即便是移动端,某些老版本的浏览器依然不支持promise,可使用es6-promise或者promise-polyfill
在对webpack做者Tobias
的采访中,当被问及可否推荐几个webpack最佳实践?做者如是回答:使用按需加载。很是简单,效果很是好。
浏览器是有缓存的,代码更改后,如何让浏览器从新加载资源?
传统的作法是在全部资源连接的后面加时间戳,但这样作的坏处是只要更新一个文件,其余没有更改的文件也会由于时间戳的更新而被从新加载,不利于浏览器作缓存,如今业界比较成熟的作法是给文件名加上哈希戳,哈希戳是文件内容的一一映射,代码更改后,哈希戳也会跟着变,内容没有更改的文件哈希戳也就不会跟着变了
module.exports = { output: { filename: isDev ? '[name].js' : '[name].[chunkhash:4].js', ... }, plugins: [ new Webpack.NamedModulesPlugin(), ] } 复制代码
qd-cli遗留问题,css的哈希戳跟js的是同样的,不利于浏览器作缓存
移动端的雪碧图宽高会带有小数点致使很差处理,暂不考虑(若是你有好的方案,欢迎提供)。太小的图片能够转成base64格式内联进文件内,另外,可使用image-webpack-loader
压缩图片,配置以下:
module.exports = { module: { rule: { test: /\.(png|svga?|jpg|gif)$/, use: [ { loader: 'url-loader', options: { limit: 8192, fallback: 'file-loader' } } ].concat(isDev ? [] : [ { loader: 'image-webpack-loader', options: { pngquant: { speed: 4, quality: '75-90' }, optipng: { optimizationLevel: 7 }, mozjpeg: { quality: 70, progressive: true }, gifsicle: { interlaced: false } } } ]) } } } 复制代码
css的抽取能够减小页面入口的体积,也能够便于css的缓存,使用官方推荐的mini-css-extract-plugin
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { module: { rules: [ { test: /\.scss$/, use: [ isDev ? 'vue-style-loader' : MiniCssExtractPlugin.loader, 'css-loader', { loader: 'postcss-loader', options: { config: { path: ownDir('lib/config/postcss.config.js') } } }, 'sass-loader' ] } ] } } 复制代码
webpack4.6+
支持资源预拉取(prefetch)
与资源预加载(preload)
,因为没有尝试成功,这里不作介绍,详情请看code-splitting
相比之前,webpack4
自己就已经快不少了,这里使用happypack
,happypack
启动多个进程加速webpack
的打包,代码以下:
const os = require('os') const HappyPack = require('happypack') const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }) module.exports = { plugins: [ new HappyPack({ id: 'eslint', verbose: false, loaders: [ ... ], threadPool: happyThreadPool }) ] } 复制代码
社区不少文章会建议使用ddl
打包方式去加速webpack的打包,能够查看:完全解决Webpack打包性能问题,因为对这个概念不是很理解,暂不作整合
为了进一步的编写脚手架,先定好项目的目录结构,这样才会有方向去编写
+ vue-project
+ src
- index.js
index.art // 每个xxx.art对应src目录的xxx.js,开发多页应用只须要增长这两个文件
mock.config.js // 必须:mock服务的配置文件
config.js // 必须:配置文件
复制代码
使用art-template
做为模板工具,使用art-template
纯粹是由于我比较熟悉,使用其余模板也是能够的,每个xxx.art对应src目录的xxx.js,开发多页应用只须要增长对应的两个文件就能够了,代码的写做思路是须要entry
入口有xxx.js
,而后plugins
属性有对应的html-webpack-plugin
,代码以下:
const glob = require('globa') const entry = {} const htmlPlugins = [] glob.sync(cwd('./src/*.@(js|jsx)')).forEach((filePath) => { const name = path.basename(filePath, path.extname(filePath)) const artPath = cwd(`${name}.art`) if (fs.existsSync(artPath)) { htmlPlugins.push(new HtmlWebpackPlugin({ filename: `${name}.html`, template: artPath })) } entry[name] = filePath }) module.exports = { entry, plugins: [...].concat(htmlPlugins) } 复制代码
目前只考虑移动端项目,提及移动端,首先要考虑的即是适配方案,这里选择大漠
大神推荐的vw布局方案,配置项有点多,这里不贴了,按照流程走没遇到什么问题
由于我对vue比较熟悉,这里选用了vue,实际上要支持react也只需针对react技术栈作一点点的改动便可,使用vue-loader,参照文档,支持了pug
语法,stylus
, scss
,文档很是的详细,配置项太多了这里不贴了,有兴趣能够直接看源码:qd-cli
支持es6,同时支持async,await,以及装饰器,这两款语法都比较实用,社区不少文章都有介绍
module.exports = { presets: [ [ 'env', // 启动tree shaking { modules: false } ], 'stage-2' ], plugins: [ 'transform-runtime', // async await 'transform-decorators-legacy' // 装饰器 ] } 复制代码
使用比较宽松的standard规范,如下是eslint的配置文件
{ extends: [ 'standard', 'plugin:vue/essential' ], rules: { 'no-unused-vars': 1, // 引入未经使用的模块的时候弹出警告而不是报错中断编译,我特别烦no-unused-vars的报错,特别是在debug的时候- -! 'no-new': 0 // 容许使用new }, // 不加这一项的话遇到懒加载,async await这样的特性eslint会报错 parserOptions: { parser: 'babel-eslint', ecmaVersion: 2017, sourceType: 'module' }, plugins: [ 'vue' ] } 复制代码
mock数据颇有意义,在与后端定好接口后,前端能够经过mock服务器生成假数据编写显示逻辑,这里使用本身撸的轮子easy-config-mock,很容易继承到现有的脚手架中,支持mock服务的自动重启,支持mockjs库的模拟数据格式,支持使用自定义中间件去编写数据返回逻辑
const EasyConfigMock = require('easy-config-mock'); new EasyConfigMock({ path: cwd('mock.config.js') }) 复制代码
mock.config.js
的demo以下:
// mock.config.js module.exports = { // common选项不是必须的,能够不用有该选项,内置的配置以下,固然你也能够更改 common: { // mock服务的默认端口,若是端口被占用,会自动换一个 port: 8018, // 若是你想看一下ajax的loading效果,该配置项能够设置接口的返回延迟 timeout: 500, // 若是你想看一下接口请求失败的效果,将rate设置成0就能够了,rate取值范围0~1,表明成功的几率 rate: 1, // 默认是true,自动开启mock服务,固然你也能够经过将其设置为false,关闭掉mock服务 mock: true }, // 普通的api... '/pkApi/getList': { code: 0, 'data|5': [{ 'uid|1000-99999': 999, 'name': '@cname' }], result: true }, // 中间件api(标准的express中间件),这里你能够书写接口返回逻辑 ['/pkApi/getOther'] (req, res, next) { const id = req.query.id req.myData = { // 重要! 将返回数据挂载在req.myData 0: { code: 0, 'test|1-100': 100 }, 1: { code: 1, 'number|+1': 202 }, 2:{ code: 2, 'name': '@cname' } }[id] next() // 最后不要忘记手动调用一下next,否则接口就暂停处理了! } } 复制代码
实现原理这里有介绍:从零开始搭建一个mock服务
项目集的结构能够以下:
+ vue-projects
- project1
- project2
+ project3
+ src
index.js
...
index.art
config.js // 项目配置
mock.config.js // 项目的mock服务
README.md // 项目的说明文档
...
- web_modules // 项目集的公共模块
config.js // 项目集配置
README.md // 项目集的说明文档
复制代码
每一个小项目都有本身config.js
配置文件与README.md
说明文档,每一个项目集一样都有本身的config.js
配置文件与README.md
说明文档,小项目的配置文件里的配置能够覆盖掉项目集的配置,另外,还有webpack_modules
目录,存放每一个项目均可以去使用的公共模块,这样作的好处是同类型项目能够丢在一块儿,而且相同的依赖,模块能够丢在web_modules
中,当web_modules
的文件发生变化,须要发版的时候,后续的持续集成能够统一处理,一键所有发版
生成最终配置文件的代码以下:
const R = require('ramda') const cwd = file => path.resolve(file || '') const generateConfig = path => { const cfg = require(cwd(path)) if (typeof cfg === 'function') { return cfg({}) } else { return cfg } } module.exports = { getConfig: R.memoize(_ => { let config = {} // 若是是项目集,项目集也会有个config.js if (fs.existsSync('../config.js')) { config = R.merge(config, generateConfig('../config')) } config = R.merge(config, generateConfig('config.js')) return config }) } 复制代码
目前只支持如下配置项
// config.js module.exports = { // 标准的webpack4的配置,能够覆盖默认配置 webpack: {}, // 默认的启动端口是8018,这里能够切换 port: 8017, // 默认设计图宽度是750,这里能够修改 viewportWidth: 750, viewportHeight: 1334, // 生产环境sourcemap使用'source-map'固定不变,开发环境能够经过devtool去设置 devtool: 'inline-source-map', // webpack-dev-server代理设置 proxy: {}, // eslint的规则,由于我本身的习惯,将'no-unused-vars'设成了1,这个配置项能够修改默认的 rules: {}, // postcss的插件,若是自行定制,本地也需安装一下相应node模块 postcssPlugin: {}, // .eslintrc的配置项,能够覆盖 eslintConfig: {}, // babel插件, 默认已经有transform-runtime与transform-decorators-legacy,请不要重复添加 babelPlugins: [], // babel preset,默认已经有env与stage-2,请不要重复添加 babelPresets: [] } 复制代码
到这里就差很少了,接下来须要将使用webpack搭建的工做流集成成cli,这样作的好处一是能够经过命令行去开发以及构建,同时,能够发布npm社区后,只需一次安装便可,便可屡次使用,由于qd-cli
内内置vue,vuex,vue-router,axios,jsonp,ramda,jquery
等模块,无需二次安装,大大减小了项目体积,简要说明集成成cli是怎么作到以及一些注意点
使用commander搭建cli,能够直接看qd-cli源码,主要代码在bin
以及lib/command
目录下,也能够参考基于node.js的脚手架工具开发经历
webpack的配置项resolve.modules
表明当require
一个文件,从这些目录去检索,qd-cli
的配置项以下
const cwd = p => path.resolve(__dirname, p) const ownDir = p => path.join(__dirname, p) module.exports = { resolve: { modules: [cwd(), cwd('node_modules'), ownDir('node_modules'), cwd('../web_modules')] } 复制代码
好比: require('jquery')
在当前项目目录找不到的话,会前往当前目录下的node_modules
,还没找到的话去前往脚手架目录下的node_modules
, 以及上一层目录下的web_modules
(项目集支持), 因为脚手架内安装了jquery
,项目自己就不须要再安装了,直接依赖便可
resolveLoader
选项,配置以下:resolveLoader: { modules: [cwd('node_modules'), ownDir('node_modules')] }, 复制代码
主要是webpack
会报错,说是找不到对应的loader
,这里要在查找loader
的路径列表里加上脚手架目录下的node_modules
bin
字段指定qd
命令对应的可执行文件的位置
"bin": { "qd": "./bin/cli.js" // 指示cli的执行文件 } 复制代码
./bin/cli.js
最上面一行#!/usr/bin/env node 复制代码
指示用什么程序去启动脚本,咱们用的是node
参考如何发布一个自定义Node.js模块到NPM(详细步骤,附Git使用方法)
因为qd-cli
的名字npm
社区不给注册(已经有类似名字的仓库了),我换成了qd-clis
😂
npm i qd-clis -g
or
yarn global add qd-clis
复制代码
window平台请使用管理员权限安装,mac平台请在命令前面加上sudo
若是你不想全局安装的话,拉到本地随意的目录并查看源码的话,能够:(一样要以管理员身份)
git clone git@github.com:nwa2018/qd-cli.git cd qd-cli npm i / yarn npm link 复制代码
安装完毕后,在命令输入qd
便可看到全部命令简介,以下图
如上图,qd-cli
具有最基础的生成种子项目,开发与构建三大功能
webpack guide
的tree-shaking章节建议在package.json
加上
"sideEffects": [ "*.css" ] 复制代码
以免css
文件被莫名地删掉,实际上结合了vue-loader
便会被删掉,解决方案是去掉该选项便可
webpack
与webpack-dev-server
命令我是使用shelljs
去启动打包与开启服务器的动做的,代码以下
// build.js... shell.exec(`${ownDir('node_modules/webpack/bin/webpack.js')} --config ${ownDir('lib/webpack/webpack.prod.js')} --progress --report`) // dev.js... shell.exec(`${ownDir('node_modules/webpack-dev-server/bin/webpack-dev-server.js')} --config ${ownDir('webpack/webpack.dev.js')} --color`) 复制代码
mac平台下没问题,window平台下直接在个人sublime打开了webpack.dev.js
与webpack.prod.js
- -!,猜想是window平台下系统不知道该以何种程序去启动文件,改为以下便可,加上node
// build.js... shell.exec(`node ${ownDir('node_modules/webpack/bin/webpack.js')} --config ${ownDir('lib/webpack/webpack.prod.js')} --progress --report`) // dev.js... shell.exec(`node ${ownDir('node_modules/webpack-dev-server/bin/webpack-dev-server.js')} --config ${ownDir('webpack/webpack.dev.js')} --color`) 复制代码
参考Parse error with import() #7764 与'Parsing error: Unexpected token function' using async/await + ecmaVersion 2017 #8366
一开始报错我觉得是babel的问题,花了不少时间去定位- -!在.eslintrc
中加上以下配置与安装babel-eslint
便可
parserOptions: { parser: 'babel-eslint', ecmaVersion: 2017, sourceType: 'module' } 复制代码
在.babelrc
里加上以下配置,我改为了babel.js
,并跟postcss,eslint的配置一块儿丢到webpack/config/
目录下,实际上babel.js
就是咱们平时编写的.babelrc
{ // 传进去babel配置路径 filename: ownDir('lib/webpack/config/babel.js'), } 复制代码
github地址,这么长的文章都看完了,走过路过的帅哥美女,点个赞呗😂😂
本文完。