写代码的小可爱

写代码的小可爱 查看完整档案

北京编辑中国地质大学(北京)  |  计算机科学与技术 编辑京东商城  |  前端开发工程师 编辑填写个人主网站
编辑

写代码的小可爱❤️

个人动态

写代码的小可爱 赞了文章 · 10月19日

走进Vue-cli源码,自己动手搭建前端脚手架工具

前言

前段时间看了一些vue-cli的源码,收获颇深。本想找个时间更新一篇文章,但是最近事情比较多,没有时间去整理这些东西。趁这两天闲了下来,便整理了一下,然后跟大家分享一下。如果小伙伴们读完之后,跟我一样收获很多的话,还望各位小伙伴们多多点赞收藏支持一下哦。

Vue-cli介绍

Vue-cli是一款非常优秀的用于迅速构建基于Vue的Web应用工具。他不同于creat-react-app这样的工具,开发者只需要关注项目逻辑的代码,而不需要关心webpack打包、启动Node服务等等诸如此类的这些问题。Vue-cli是一款基于模板化的开发工具,等于就是把别人的项目结构给照搬过来,所有的配置都是暴露出来的,你可以根据实际情况去做一些配置的修改,更加灵活自由一点。当然这对前端工程师提出更高的要求,考虑的东西也变多了。不过Vue-cli即将发布3.0的版本,整个Vue-cli发生了翻天覆地的变化,它采用跟creat-react-app这类工具的模式,开发者只需要关注项目逻辑的代码即可。不过目前3.0还没有出来,所以这次源码分析我采用的v2.9.3的源码,也就是2.0的代码。后面小伙们在阅读的时候要注意以下。

Vue-cli项目结构

图片描述

整个项目的目录结构如上图所示,下面我大概介绍每个文件夹的东西大致都是干嘛的。

  • bin //这里放的vue的一些命令文件,比如vue init这样的命令都是从由这里控制的
  • docs //一些注意事项啥的,不重要的目录,可以直接忽略
  • lib //这里存放着一些vue-cli需要的一些自定义方法
  • node_modules //这里就不用我多说了
  • test // 单元测试 开会vue-cli工具时会用到,我们读源码的时候可以直接忽略掉
  • 一些杂七杂八的东西 //比如eslint配置、.gitignore、LICENSE等等诸如此类这些东西。不影响阅读源码,直接忽略掉。
  • package.json/README.md //这个也不用我多说了,大家都知道的
综合来说,我们阅读源码所要关注的只有bin和lib下面即可,其他的都可忽略。下面开始阅读之旅吧

Vue-cli源码阅读之旅

在开始读源码之前,首先我要介绍一个工具(commander),这是用来处理命令行的工具。具体的使用方法可查看github的README.md https://github.com/tj/command... 。小伙伴们再阅读后面的内容之前,建议先去了解一下commander,方便后续的理解。这里我们对commander就不做详细介绍了。这里vue-cli采用了commander的git风格的写法。vue文件处理vue命令,vue-init处理vue init命令以此类推。接着我们一个一个命令看过去。

vue

引入的包:

  • commander //用于处理命令行

作用: vue这个文件代码很少,我就直接贴出来了。

#!/usr/bin/env node

require('commander')
  .version(require('../package').version)
  .usage('<command> [options]')
  .command('init', 'generate a new project from a template')
  .command('list', 'list available official templates')
  .command('build', 'prototype a new project')
  .parse(process.argv)

这个文件主要是在用户输入“vue”时,终端上显示参数的使用说明。具体的写法可参考 https://github.com/tj/command... 上面的说明。

vue build

引入的包:

  • chalk //用于高亮终端打印出来的信息

作用:vue build命令在vue-cli之中已经删除了,源码上做了一定的说明。代码不多,我就直接贴出来。


const chalk = require('chalk')

console.log(chalk.yellow(
  '\n' +
  '  We are slimming down vue-cli to optimize the initial installation by ' +
  'removing the `vue build` command.\n' +
  '  Check out Poi (https://github.com/egoist/poi) which offers the same functionality!' +
  '\n'
))

vue list

#!/usr/bin/env node
const logger = require('../lib/logger')
const request = require('request')
const chalk = require('chalk')

/**
 * Padding.
 */

console.log()
process.on('exit', () => {
  console.log()
})

/**
 * List repos.
 */

request({
  url: 'https://api.github.com/users/vuejs-templates/repos',
  headers: {
    'User-Agent': 'vue-cli'
  }
}, (err, res, body) => {
  if (err) logger.fatal(err)
  const requestBody = JSON.parse(body)
  if (Array.isArray(requestBody)) {
    console.log('  Available official templates:')
    console.log()
    requestBody.forEach(repo => {
      console.log(
        '  ' + chalk.yellow('★') +
        '  ' + chalk.blue(repo.name) +
        ' - ' + repo.description)
    })
  } else {
    console.error(requestBody.message)
  }
})

引入的包:

  • request //发送http请求的工具。
  • chalk //用于高亮console.log打印出来的信息。
  • logger //自定义工具-用于日志打印。

作用:当输入"vue list"时(我们测试时,可以直接在当前源码文件目录下的终端上输入“bin/vue-list”),vue-cli会请求接口,获取官方模板的信息,然后做了一定处理,在终端上显示出来模板名称和对应的说明。

效果如下:

  Available official templates:

  ★  browserify - A full-featured Browserify + vueify setup with hot-reload, linting & unit testing.
  ★  browserify-simple - A simple Browserify + vueify setup for quick prototyping.
  ★  pwa - PWA template for vue-cli based on the webpack template
  ★  simple - The simplest possible Vue setup in a single HTML file
  ★  webpack - A full-featured Webpack + vue-loader setup with hot reload, linting, testing & css extraction.
  ★  webpack-simple - A simple Webpack + vue-loader setup for quick prototyping.

vue init

vue init”是用来构建项目的命令,也是vue-cli的核心文件,上面的三个都是非常简单的命令,算是我们阅读源码的开胃菜,真正的大餐在这里。

工作流程

在讲代码之前,首先我们要讲一下整个vue-cli初始项目的流程,然后我们沿着流程一步一步走下去。

图片描述

整个vue init大致流程如我上图所示,应该还是比较好理解的。这里我大致阐述一下大致的流程。

  1. vue-cli会先判断你的模板在远程github仓库上还是在你的本地某个文件里面,若是本地文件夹则会立即跳到第3步,反之则走第2步。
  2. 第2步会判断是否为官方模板,官方模板则会从官方github仓库中下载模板到本地的默认仓库下,即根目录下.vue-templates文件夹下。
  3. 第3步则读取模板目录下meta.js或者meta.json文件,根据里面的内容会询问开发者,根据开发者的回答,确定一些修改。
  4. 根据模板内容以及开发者的回答,渲染出项目结构并生成到指定目录。

源码内容

这里vue-init文件的代码比较多,我这里就拆分几块来看。首先我先把整个文件的结构列出来,方便后续的阅读。

  /**
   * 引入一大堆包
   */
    const program = require('commander')
    ...
  
   
   /**
    * 配置commander的使用方法
    */     
    
    program
      .usage('<template-name> [project-name]')
      .option('-c, --clone', 'use git clone')
      .option('--offline', 'use cached template')
      
  /**
    * 定义commander的help方法
    */  
    program.on('--help', () => {
      console.log('  Examples:')
      console.log()
      console.log(chalk.gray('    # create a new project with an official template'))
      console.log('    $ vue init webpack my-project')
      console.log()
      console.log(chalk.gray('    # create a new project straight from a github template'))
      console.log('    $ vue init username/repo my-project')
      console.log()
    })
    
    
    function help () {
      program.parse(process.argv)
      if (program.args.length < 1) return program.help() //如果没有输入参数,终端显示帮助
    }
    help()
    
    /**
     * 定义一大堆变量
     */
     
     let template = program.args[0]
     ...
     
     /**
      * 判断是否输入项目名  是 - 直接执行run函数  否- 询问开发者是否在当前目录下生成项目,开发者回答“是” 也执行run函数 否则不执行run函数
      */
     
     /**
     * 定义主函数 run
     */
     function run (){
         ...
     }
     
     /**
      * 定义下载模板并生产项目的函数 downloadAndGenerate
      */
      function downloadAndGenerate(){
          ...
      }

整个文件大致的东西入上面所示,后面我们将一块一块内容来看。

引入的一堆包

const download = require('download-git-repo')  //用于下载远程仓库至本地 支持GitHub、GitLab、Bitbucket
const program = require('commander') //命令行处理工具
const exists = require('fs').existsSync  //node自带的fs模块下的existsSync方法,用于检测路径是否存在。(会阻塞)
const path = require('path') //node自带的path模块,用于拼接路径
const ora = require('ora') //用于命令行上的加载效果
const home = require('user-home')  //用于获取用户的根目录
const tildify = require('tildify') //将绝对路径转换成带波浪符的路径
const chalk = require('chalk')// 用于高亮终端打印出的信息
const inquirer = require('inquirer') //用于命令行与开发者交互
const rm = require('rimraf').sync // 相当于UNIX的“rm -rf”命令
const logger = require('../lib/logger') //自定义工具-用于日志打印
const generate = require('../lib/generate')  //自定义工具-用于基于模板构建项目
const checkVersion = require('../lib/check-version') //自定义工具-用于检测vue-cli版本的工具
const warnings = require('../lib/warnings') //自定义工具-用于模板的警告
const localPath = require('../lib/local-path') //自定义工具-用于路径的处理

const isLocalPath = localPath.isLocalPath  //判断是否是本地路径
const getTemplatePath = localPath.getTemplatePath  //获取本地模板的绝对路径

定义的一堆变量

let template = program.args[0]  //模板名称
const hasSlash = template.indexOf('/') > -1   //是否有斜杠,后面将会用来判定是否为官方模板   
const rawName = program.args[1]  //项目构建目录名
const inPlace = !rawName || rawName === '.'  // 没写或者“.”,表示当前目录下构建项目
const name = inPlace ? path.relative('../', process.cwd()) : rawName  //如果在当前目录下构建项目,当前目录名为项目构建目录名,否则是当前目录下的子目录【rawName】为项目构建目录名
const to = path.resolve(rawName || '.') //项目构建目录的绝对路径
const clone = program.clone || false  //是否采用clone模式,提供给“download-git-repo”的参数

const tmp = path.join(home, '.vue-templates', template.replace(/[\/:]/g, '-'))  //远程模板下载到本地的路径

主逻辑

if (inPlace || exists(to)) {
  inquirer.prompt([{
    type: 'confirm',
    message: inPlace
      ? 'Generate project in current directory?'
      : 'Target directory exists. Continue?',
    name: 'ok'
  }]).then(answers => {
    if (answers.ok) {
      run()
    }
  }).catch(logger.fatal)
} else {
  run()
}

对着上面代码,vue-cli会判断inPlace和exists(to),true则询问开发者,当开发者回答“yes”的时候执行run函数,否则直接执行run函数。这里询问开发者的问题有如下两个:

  • Generate project in current directory? //是否在当前目录下构建项目
  • Target directory exists. Continue? //构建目录已存在,是否继续

这两个问题依靠变量inPlace来选择,下面我看一下变量inPlace是怎么得来的。

const rawName = program.args[1]  //rawName为命令行的第二个参数(项目构建目录的相对目录)
const inPlace = !rawName || rawName === '.'  //rawName存在或者为“.”的时候,视为在当前目录下构建

通过上面的描述可知,变量inPlace用于判断是否在当前目录下构建,因此变量inPlace为true时,则会提示Generate project in current directory? ,反之当变量inPlace为false时,此时exists(to)一定为true,便提示Target directory exists. Continue?

Run函数

逻辑:

图片描述

源码:

function run () {
  // check if template is local
  if (isLocalPath(template)) {    //是否是本地模板
    const templatePath = getTemplatePath(template)  //获取绝对路径
    if (exists(templatePath)) {  //判断模板所在路径是否存在
       //渲染模板
      generate(name, templatePath, to, err => {
        if (err) logger.fatal(err)
        console.log()
        logger.success('Generated "%s".', name)
      })
    } else {
       //打印错误日志,提示本地模板不存在
      logger.fatal('Local template "%s" not found.', template)
    }
  } else {
    checkVersion(() => {  //检查版本号
      if (!hasSlash) {  //官方模板还是第三方模板
        // use official templates
        // 从这句话以及download-git-repo的用法,我们得知了vue的官方的模板库的地址:https://github.com/vuejs-templates
        const officialTemplate = 'vuejs-templates/' + template
        if (template.indexOf('#') !== -1) {  //模板名是否带"#"
          downloadAndGenerate(officialTemplate) //下载模板
        } else {
          if (template.indexOf('-2.0') !== -1) { //是都带"-2.0"
             //发出警告
            warnings.v2SuffixTemplatesDeprecated(template, inPlace ? '' : name)
            return
          }

          // warnings.v2BranchIsNowDefault(template, inPlace ? '' : name)
          downloadAndGenerate(officialTemplate)//下载模板
        }
      } else {
        downloadAndGenerate(template)//下载模板
      }
    })
  }
}

downloadAndGenerate函数

function downloadAndGenerate (template) {
  const spinner = ora('downloading template')  
  spinner.start()//显示加载状态
  // Remove if local template exists
  if (exists(tmp)) rm(tmp)  //当前模板库是否存在该模板,存在就删除
   //下载模板  template-模板名    tmp- 模板路径   clone-是否采用git clone模板   err-错误短信
    
  download(template, tmp, { clone }, err => {
    spinner.stop() //隐藏加载状态
    //如果有错误,打印错误日志
    if (err) logger.fatal('Failed to download repo ' + template + ': ' + err.message.trim())
    //渲染模板
    generate(name, tmp, to, err => {
      if (err) logger.fatal(err)
      console.log()
      logger.success('Generated "%s".', name)
    })
  })
}

lib

generate.js (★)

lib文件下最重要的js文件,他是我们构建项目中最重要的一环,根据模板渲染成我们需要的项目。这块内容是需要我们重点关注的。

const chalk = require('chalk')
const Metalsmith = require('metalsmith')
const Handlebars = require('handlebars')
const async = require('async')
const render = require('consolidate').handlebars.render
const path = require('path')
const multimatch = require('multimatch')
const getOptions = require('./options')
const ask = require('./ask')
const filter = require('./filter')
const logger = require('./logger')

// register handlebars helper  注册handlebars的helper
Handlebars.registerHelper('if_eq', function (a, b, opts) {
  return a === b
    ? opts.fn(this)
    : opts.inverse(this)
})

Handlebars.registerHelper('unless_eq', function (a, b, opts) {
  return a === b
    ? opts.inverse(this)
    : opts.fn(this)
})

/**
 * Generate a template given a `src` and `dest`.
 *
 * @param {String} name
 * @param {String} src
 * @param {String} dest
 * @param {Function} done
 */

module.exports = function generate (name, src, dest, done) {
  const opts = getOptions(name, src)  //获取配置
  const metalsmith = Metalsmith(path.join(src, 'template'))  //初始化Metalsmith对象
  const data = Object.assign(metalsmith.metadata(), {
    destDirName: name,
    inPlace: dest === process.cwd(),
    noEscape: true
  })//添加一些变量至metalsmith中,并获取metalsmith中全部变量
  
  //注册配置对象中的helper
  opts.helpers && Object.keys(opts.helpers).map(key => {
    Handlebars.registerHelper(key, opts.helpers[key])
  })

  const helpers = { chalk, logger }

 //配置对象是否有before函数,是则执行
  if (opts.metalsmith && typeof opts.metalsmith.before === 'function') {
    opts.metalsmith.before(metalsmith, opts, helpers)
  }

  metalsmith.use(askQuestions(opts.prompts))  //询问问题
    .use(filterFiles(opts.filters))  //过滤文件
    .use(renderTemplateFiles(opts.skipInterpolation)) //渲染模板文件


  //配置对象是否有after函数,是则执行
  if (typeof opts.metalsmith === 'function') {
    opts.metalsmith(metalsmith, opts, helpers)
  } else if (opts.metalsmith && typeof opts.metalsmith.after === 'function') {
    opts.metalsmith.after(metalsmith, opts, helpers)
  }

  metalsmith.clean(false) 
    .source('.') // start from template root instead of `./src` which is Metalsmith's default for `source`
    .destination(dest)
    .build((err, files) => {
      done(err)
      if (typeof opts.complete === 'function') {
      //配置对象有complete函数则执行
        const helpers = { chalk, logger, files }
        opts.complete(data, helpers)
      } else {
      //配置对象有completeMessage,执行logMessage函数
        logMessage(opts.completeMessage, data)
      }
    })

  return data
}

/**
 * Create a middleware for asking questions.
 *
 * @param {Object} prompts
 * @return {Function}
 */

function askQuestions (prompts) {
  return (files, metalsmith, done) => {
    ask(prompts, metalsmith.metadata(), done)
  }
}

/**
 * Create a middleware for filtering files.
 *
 * @param {Object} filters
 * @return {Function}
 */

function filterFiles (filters) {
  return (files, metalsmith, done) => {
    filter(files, filters, metalsmith.metadata(), done)
  }
}

/**
 * Template in place plugin.
 *
 * @param {Object} files
 * @param {Metalsmith} metalsmith
 * @param {Function} done
 */

function renderTemplateFiles (skipInterpolation) {
  skipInterpolation = typeof skipInterpolation === 'string'
    ? [skipInterpolation]
    : skipInterpolation    //保证skipInterpolation是一个数组
  return (files, metalsmith, done) => {
    const keys = Object.keys(files) //获取files的所有key
    const metalsmithMetadata = metalsmith.metadata() //获取metalsmith的所有变量
    async.each(keys, (file, next) => { //异步处理所有files
      // skipping files with skipInterpolation option  
      // 跳过符合skipInterpolation的要求的file
      if (skipInterpolation && multimatch([file], skipInterpolation, { dot: true }).length) {
        return next()
      }
      //获取文件的文本内容
      const str = files[file].contents.toString()
      // do not attempt to render files that do not have mustaches
      //跳过不符合handlebars语法的file
      if (!/{{([^{}]+)}}/g.test(str)) {  
        return next()
      }
      //渲染文件
      render(str, metalsmithMetadata, (err, res) => {
        if (err) {
          err.message = `[${file}] ${err.message}`
          return next(err)
        }
        files[file].contents = new Buffer(res)
        next()
      })
    }, done)
  }
}

/**
 * Display template complete message.
 *
 * @param {String} message
 * @param {Object} data
 */

function logMessage (message, data) {
  if (!message) return  //没有message直接退出函数
  render(message, data, (err, res) => {
    if (err) {
      console.error('\n   Error when rendering template complete message: ' + err.message.trim())  //渲染错误打印错误信息
    } else {
      console.log('\n' + res.split(/\r?\n/g).map(line => '   ' + line).join('\n'))
      //渲染成功打印最终渲染的结果
    }
  })
}

引入的包:

  • chalk //用于高亮终端打印出来的信息。
  • metalsmith //静态网站生成器。
  • handlebars //知名的模板引擎。
  • async //非常强大的异步处理工具。
  • consolidate //支持各种模板引擎的渲染。
  • path //node自带path模块,用于路径的处理。
  • multimatch // 可以支持多个条件的匹配。
  • options //自定义工具-用于获取模板配置。
  • ask //自定义工具-用于询问开发者。
  • filter //自定义工具-用于文件过滤。
  • logger //自定义工具-用于日志打印。

主逻辑:

获取模板配置 -->初始化Metalsmith -->添加一些变量至Metalsmith -->handlebars模板注册helper -->配置对象中是否有before函数,有则执行 -->询问问题 -->过滤文件 -->渲染模板文件 -->配置对象中是否有after函数,有则执行 -->最后构建项目内容 -->构建完成,成功若配置对象中有complete函数则执行,否则打印配置对象中的completeMessage信息,如果有错误,执行回调函数done(err)

其他函数:

  • askQuestions: 询问问题
  • filterFiles: 过滤文件
  • renderTemplateFiles: 渲染模板文件
  • logMessage: 用于构建成功时,打印信息

Metalsmith插件格式:

function <function name> {
  return (files,metalsmith,done)=>{
    //逻辑代码
    ...
  }
}

options.js

const path = require('path')
const metadata = require('read-metadata')
const exists = require('fs').existsSync
const getGitUser = require('./git-user')
const validateName = require('validate-npm-package-name')

/**
 * Read prompts metadata.
 *
 * @param {String} dir
 * @return {Object}
 */

module.exports = function options (name, dir) {
  const opts = getMetadata(dir)

  setDefault(opts, 'name', name)
  setValidateName(opts)

  const author = getGitUser()
  if (author) {
    setDefault(opts, 'author', author)
  }

  return opts
}

/**
 * Gets the metadata from either a meta.json or meta.js file.
 *
 * @param  {String} dir
 * @return {Object}
 */

function getMetadata (dir) {
  const json = path.join(dir, 'meta.json')
  const js = path.join(dir, 'meta.js')
  let opts = {}

  if (exists(json)) {
    opts = metadata.sync(json)
  } else if (exists(js)) {
    const req = require(path.resolve(js))
    if (req !== Object(req)) {
      throw new Error('meta.js needs to expose an object')
    }
    opts = req
  }

  return opts
}

/**
 * Set the default value for a prompt question
 *
 * @param {Object} opts
 * @param {String} key
 * @param {String} val
 */

function setDefault (opts, key, val) {
  if (opts.schema) {
    opts.prompts = opts.schema
    delete opts.schema
  }
  const prompts = opts.prompts || (opts.prompts = {})
  if (!prompts[key] || typeof prompts[key] !== 'object') {
    prompts[key] = {
      'type': 'string',
      'default': val
    }
  } else {
    prompts[key]['default'] = val
  }
}

function setValidateName (opts) {
  const name = opts.prompts.name
  const customValidate = name.validate
  name.validate = name => {
    const its = validateName(name)
    if (!its.validForNewPackages) {
      const errors = (its.errors || []).concat(its.warnings || [])
      return 'Sorry, ' + errors.join(' and ') + '.'
    }
    if (typeof customValidate === 'function') return customValidate(name)
    return true
  }
}

引入的包:

  • path //node自带path模块,用于路径的处理
  • read-metadata //用于读取json或者yaml元数据文件并返回一个对象
  • fs.existsSync //node自带fs模块的existsSync方法,用于检测路径是否存在
  • git-user //获取本地的git配置
  • validate-npm-package-name //用于npm包的名字是否是合法的

作用:

  • 主方法: 第一步:先获取模板的配置文件信息;第二步:设置name字段并检测name是否合法;第三步:只是author字段。
  • getMetadata: 获取meta.js或则meta.json中的配置信息
  • setDefault: 用于向配置对象中添加一下默认字段
  • setValidateName: 用于检测配置对象中name字段是否合法

git-user.js

const exec = require('child_process').execSync

module.exports = () => {
  let name
  let email

  try {
    name = exec('git config --get user.name')
    email = exec('git config --get user.email')
  } catch (e) {}

  name = name && JSON.stringify(name.toString().trim()).slice(1, -1)
  email = email && (' <' + email.toString().trim() + '>')
  return (name || '') + (email || '')
}

引入的包:

  • child_process.execSync //node自带模块child_process中的execSync方法用于新开一个shell并执行相应的command,并返回相应的输出。

作用: 用于获取本地的git配置的用户名和邮件,并返回格式 姓名<邮箱> 的字符串。

eval.js

const chalk = require('chalk')

/**
 * Evaluate an expression in meta.json in the context of
 * prompt answers data.
 */

module.exports = function evaluate (exp, data) {
  /* eslint-disable no-new-func */
  const fn = new Function('data', 'with (data) { return ' + exp + '}')
  try {
    return fn(data)
  } catch (e) {
    console.error(chalk.red('Error when evaluating filter condition: ' + exp))
  }
}

引入的包:

  • chalk //用于高亮终端打印出来的信息。

作用: 在data的作用域执行exp表达式并返回其执行得到的值

ask.js

const async = require('async')
const inquirer = require('inquirer')
const evaluate = require('./eval')

// Support types from prompt-for which was used before
const promptMapping = {
  string: 'input',
  boolean: 'confirm'
}

/**
 * Ask questions, return results.
 *
 * @param {Object} prompts
 * @param {Object} data
 * @param {Function} done
 */
 
/**
 * prompts meta.js或者meta.json中的prompts字段
 * data metalsmith.metadata()
 * done 交于下一个metalsmith插件处理
 */
module.exports = function ask (prompts, data, done) {
 //遍历处理prompts下的每一个字段
  async.eachSeries(Object.keys(prompts), (key, next) => {
    prompt(data, key, prompts[key], next)
  }, done)
}

/**
 * Inquirer prompt wrapper.
 *
 * @param {Object} data
 * @param {String} key
 * @param {Object} prompt
 * @param {Function} done
 */

function prompt (data, key, prompt, done) {
  // skip prompts whose when condition is not met
  if (prompt.when && !evaluate(prompt.when, data)) {
    return done()
  }

  //获取默认值
  let promptDefault = prompt.default
  if (typeof prompt.default === 'function') {
    promptDefault = function () {
      return prompt.default.bind(this)(data)
    }
  }
  //设置问题,具体使用方法可去https://github.com/SBoudrias/Inquirer.js上面查看
  inquirer.prompt([{
    type: promptMapping[prompt.type] || prompt.type,
    name: key,
    message: prompt.message || prompt.label || key,
    default: promptDefault,
    choices: prompt.choices || [],
    validate: prompt.validate || (() => true)
  }]).then(answers => {
    if (Array.isArray(answers[key])) { 
      //当答案是一个数组时
      data[key] = {}
      answers[key].forEach(multiChoiceAnswer => {
        data[key][multiChoiceAnswer] = true
      })
    } else if (typeof answers[key] === 'string') {
     //当答案是一个字符串时
      data[key] = answers[key].replace(/"/g, '\\"')
    } else {
     //其他情况
      data[key] = answers[key]
    }
    done()
  }).catch(done)
}

引入的包:

  • async //异步处理工具。
  • inquirer //命令行与用户之间的交互
  • eval //返回某作用下表达式的值

作用: 将meta.js或者meta.json中的prompts字段解析成对应的问题询问。

filter.js

const match = require('minimatch')
const evaluate = require('./eval')
/**
 * files 模板内的所有文件
 * filters meta.js或者meta.json的filters字段
 * data metalsmith.metadata()
 * done  交于下一个metalsmith插件处理
 */
module.exports = (files, filters, data, done) => {
  if (!filters) {
    //meta.js或者meta.json没有filters字段直接跳过交于下一个metalsmith插件处理
    return done()
  }
  //获取所有文件的名字
  const fileNames = Object.keys(files)
  //遍历meta.js或者meta.json没有filters下的所有字段
  Object.keys(filters).forEach(glob => {
    //遍历所有文件名
    fileNames.forEach(file => {
      //如果有文件名跟filters下的某一个字段匹配上
      if (match(file, glob, { dot: true })) {        
        const condition = filters[glob]
        if (!evaluate(condition, data)) {
          //如果metalsmith.metadata()下condition表达式不成立,删除该文件
          delete files[file]
        }
      }
    })
  })
  done()
}

引入的包:

  • minimatch //字符匹配工具
  • eval //返回某作用下表达式的值

作用: 根据metalsmith.metadata()删除一些不需要的模板文件,而metalsmith.metadata()主要在ask.js中改变的,也就是说ask.js中获取到用户的需求。

logger.js

const chalk = require('chalk')
const format = require('util').format

/**
 * Prefix.
 */

const prefix = '   vue-cli'
const sep = chalk.gray('·')

/**
 * Log a `message` to the console.
 *
 * @param {String} message
 */

exports.log = function (...args) {
  const msg = format.apply(format, args)
  console.log(chalk.white(prefix), sep, msg)
}

/**
 * Log an error `message` to the console and exit.
 *
 * @param {String} message
 */

exports.fatal = function (...args) {
  if (args[0] instanceof Error) args[0] = args[0].message.trim()
  const msg = format.apply(format, args)
  console.error(chalk.red(prefix), sep, msg)
  process.exit(1)
}

/**
 * Log a success `message` to the console and exit.
 *
 * @param {String} message
 */

exports.success = function (...args) {
  const msg = format.apply(format, args)
  console.log(chalk.white(prefix), sep, msg)
}

引入的包:

  • chalk //用于高亮终端打印出来的信息。
  • format //node自带的util模块中的format方法。

作用: logger.js主要提供三个方法log(常规日志)、fatal(错误日志)、success(成功日志)。每个方法都挺简单的,我就不错过多的解释了。

local-path.js

const path = require('path')

module.exports = {
  isLocalPath (templatePath) {
    return /^[./]|(^[a-zA-Z]:)/.test(templatePath)
  },

  getTemplatePath (templatePath) {
    return path.isAbsolute(templatePath)
      ? templatePath
      : path.normalize(path.join(process.cwd(), templatePath))
  }
}

引入的包:

  • path //node自带的路径处理工具。

作用:

  • isLocalPath: UNIX (以“.”或者"/"开头) WINDOWS(以形如:“C:”的方式开头)。
  • getTemplatePath: templatePath是否为绝对路径,是则返回templatePath 否则转换成绝对路径并规范化。

check-version.js

const request = require('request')
const semver = require('semver')
const chalk = require('chalk')
const packageConfig = require('../package.json')

module.exports = done => {
  // Ensure minimum supported node version is used
  if (!semver.satisfies(process.version, packageConfig.engines.node)) {
    return console.log(chalk.red(
      '  You must upgrade node to >=' + packageConfig.engines.node + '.x to use vue-cli'
    ))
  }

  request({
    url: 'https://registry.npmjs.org/vue-cli',
    timeout: 1000
  }, (err, res, body) => {
    if (!err && res.statusCode === 200) {
      const latestVersion = JSON.parse(body)['dist-tags'].latest
      const localVersion = packageConfig.version
      if (semver.lt(localVersion, latestVersion)) {
        console.log(chalk.yellow('  A newer version of vue-cli is available.'))
        console.log()
        console.log('  latest:    ' + chalk.green(latestVersion))
        console.log('  installed: ' + chalk.red(localVersion))
        console.log()
      }
    }
    done()
  })
}

引入的包:

  • request //http请求工具。
  • semver //版本号处理工具。
  • chalk //用于高亮终端打印出来的信息。

作用:

  • 第一步:检查本地的node版本号,是否达到package.json文件中对node版本的要求,若低于nodepackage.json文件中要求的版本,则直接要求开发者更新自己的node版本。反之,可开始第二步。
  • 第二步: 通过请求https://registry.npmjs.org/vu...来获取vue-cli的最新版本号,跟package.json中的version字段进行比较,若本地的版本号小于最新的版本号,则提示有最新版本可以更新。这里需要注意的是,这里检查版本号并不影响后续的流程,即便本地的vue-cli版本不是最新的,也不影响构建,仅仅提示一下。

warnings.js

const chalk = require('chalk')

module.exports = {
  v2SuffixTemplatesDeprecated (template, name) {
    const initCommand = 'vue init ' + template.replace('-2.0', '') + ' ' + name

    console.log(chalk.red('  This template is deprecated, as the original template now uses Vue 2.0 by default.'))
    console.log()
    console.log(chalk.yellow('  Please use this command instead: ') + chalk.green(initCommand))
    console.log()
  },
  v2BranchIsNowDefault (template, name) {
    const vue1InitCommand = 'vue init ' + template + '#1.0' + ' ' + name

    console.log(chalk.green('  This will install Vue 2.x version of the template.'))
    console.log()
    console.log(chalk.yellow('  For Vue 1.x use: ') + chalk.green(vue1InitCommand))
    console.log()
  }
}

引入的包:

  • chalk //用于高亮终端打印出来的信息。

作用:

  • v2SuffixTemplatesDeprecated:提示带“-2.0”的模板已经弃用了,官方模板默认用2.0了。不需要用“-2.0”来区分vue1.0和vue2.0了。
  • v2BranchIsNowDefault: 这个方法在vue-init文件中已经被注释掉,不再使用了。在vue1.0向vue2.0过渡的时候用到过,现在都是默认2.0了,自然也就不用了。

总结

由于代码比较多,很多代码我就没有一一细讲了,一些比较简单或者不是很重要的js文件,我就单单说明了它的作用了。但是重点的js文件,我还是加了很多注解在上面。其中我个人认为比较重点的文件就是vue-initgenerate.jsoptions.jsask.jsfilter.js,这五个文件构成了vue-cli构建项目的主流程,因此需要我们花更多的时间在上面。另外,我们在读源码的过程中,一定要理清楚整个构建流程是什么样子的,心里得有一个谱。读完源码之后,我个人是建议自己动手搭建一个构建工具,这样的话印象才会更加深刻,个人成长会更大点。我自己在读完整个vue-cli之后,我自己根据vue-cli的流程也动手搞了一个脚手架工具,仅供大家参考学习一下。地址如下:

https://github.com/ruichengpi...

最后祝愿大家可以在前端的道路上越走越好!如果喜欢我的文章,请记得关注我哦!后续会推出更多的优质的文章哦,敬请期待!

查看原文

赞 54 收藏 68 评论 7

写代码的小可爱 发布了文章 · 9月28日

京东商城招聘Java开发工程师

Java软件开发工程师

  • 工作职责:
  1. 负责京东技术中心交易流程搜索、商详、购物车、结算、订单等核心业务系统的需求分析、设计、开发工作;
  2. 负责相关系统需求沟通、技术方案设计、文档编写、评审工作;
  3. 解决系统中的关键问题和技术难题;

4、负责相关系统PaaS化升级改造工作;

5、负责618、双11等大促备战工作,在这里你将会亲身参与顶级流量带来的技术挑战和成功带来的荣誉。

  • 岗位要求:
  1. 积极主动工作、责任心强,具备良好的沟通能力和学习能力;
  2. 本科及以上学历,计算机相关专业;
  3. 精通web开发的相关技术,有扎实的java基础,3年以上使用JAVA进行web开发的经验;
  4. 精通Java并有较全的知识面,熟悉Spring、Struts、MyBatis等主流框架;熟悉 Java,熟悉IO,多线程,集合类等基础框架;
  5. 熟悉常用设计模式(工厂模式,单例等);
  6. 熟悉Linux平台下常用命令操作、环境部署;
  7. 熟悉数据库技术(MySQL/Oracle),数据库优化及SQL优化;
  8. 熟悉缓存,消息队列等机制;
  9. 思维灵活,热爱技术,对Web设计、编程开发有丰富的经验;

10、对DDD熟悉者优先,有电商复杂系统开发经验优先,有实际PaaS化项目开发经验者优先考虑。

  • 主要业务:京东主站核心交易流程开发;
  • 招聘岗位:Java中、高级开发工程师;
  • 薪资待遇:与面试者的技术沟通评级有关,技术面试通过后具体与HR商谈;
  • 职责/技术栈:详见工作职责、岗位要求;
  • 工作强度:互联网行业常规工作方式、工作强度;
  • 公司福利:餐补,上下班班车,节日福利等;
  • 面试流程:一面电话沟通、二面到公司沟通、高T会有多轮技术沟通,技术面试通过与HR同学沟通福利待遇;
  • 在招人数:9人;
  • 学历要求:第一学历需统招本科以上,年龄期待年轻化。
  • 联系我:18701324745。欢迎投递【wanghui36@jd.com】
查看原文

赞 0 收藏 0 评论 0

写代码的小可爱 发布了文章 · 9月8日

tarojs/cli update

针对taro的升级有几种方案,如下:

  • 全局安装

    npm i @tarojs/cli -g
    taro info
    
    👽 Taro v3.0.9
    
     Taro CLI 3.0.9 environment info:
       System:
         OS: macOS High Sierra 10.13.6
         Shell: 5.7 - /usr/local/bin/zsh
       Binaries:
         Node: 10.15.2 - ~/.nvm/versions/node/v10.15.2/bin/node
         Yarn: 1.19.1 - ~/.yarn/bin/yarn
         npm: 6.14.4 - ~/.nvm/versions/node/v10.15.2/bin/npm
       npmPackages:
         @tarojs/cli: ^3.0.9 => 3.0.9 
         @tarojs/components: 3.0.9 => 3.0.9 
         @tarojs/mini-runner: 3.0.9 => 3.0.9 
         @tarojs/react: 3.0.9 => 3.0.9 
         @tarojs/runtime: 3.0.9 => 3.0.9 
         @tarojs/taro: 3.0.9 => 3.0.9 
         @tarojs/webpack-runner: 3.0.9 => 3.0.9 
         babel-preset-taro: 3.0.9 => 3.0.9 
         eslint-config-taro: 3.0.9 => 3.0.9 
         eslint-plugin-taro: ^2.2.3 => 2.2.3 
         react: ^16.10.0 => 16.13.1 
       npmGlobalPackages:
         typescript: 3.7.2
  • 安装成功就会展示最新版本
  • 在项目工程中

    npm update self 3.0.9
    taro update project 3.0.9
    npm install -D @tarojs/cli@3.0.9
  • 注意一定能要执行npm install -D @tarojs/cli@3.0.9,否则可能安装不成功
  • 还可以在package.json 中将对应需要升级的taro相关版本,替换为新版本。例如

       "@tarojs/mini-runner": "3.0.9",
       "@tarojs/webpack-runner": "3.0.9",
  • 这只是举例子,之前的版本是3.0.7 改为3.0.9 ,然后执行npm install重新安装依赖,确认 node_modules 中升级的依赖是不是已经变更为你指定安装的版本了。
  • 注意,如果安装不成功,就在本地项目中,安装指定cli版本。npm install -D @tarojs/cli@3.0.9
查看原文

赞 0 收藏 0 评论 0

写代码的小可爱 发布了文章 · 9月8日

taro 升级为3.0.9版本之后的明显的两大变化

主要谈论两点我项目中亲身经历到的,更能感同身受

一、优化base.wxml 模版的体积

  • 下边看我升级之前的base.wxml的体积,如
  • 预览包大小为 5650kb,大家都知道小程序是有大小上限的,超出之后就给开发者带来了一定的困扰。

  • 针对大小超限制,我进行了分包预加载的方案,但是无论怎么分都是不行的,因为base.wxml太大了。大的可怕。
  • 就去官方找issues,巧了,就有人遇到一样的问题。issues
主要如下三个方向
  • 只递归 view、cover-view、block、text、slot、slot-view、label、form、scroll-view 组件,其它组件只需输出一层。其中 slot、slot-view、label、form、scroll-view 组件虽然可能会循环引用自身,但不会循环太多层,因此加上了一定的循环层数限制。
  • ad、official-account、open-data、navigation-bar 不需要渲染子元素
  • 利用 wxs 计算三元表达式
  • 第 16 层只需要渲染一个 container
  • 针对如上这里有完整的提案
  • 了解此提案之后,我马上对项目进行了升级
  • 升级之后的大小来了,擦亮眼睛吧,惊喜来了

  • 看见了吗?base由3.6 MB 变成了211 KB,这多么棒呀,体积优化到了几倍,你去算算吧,哈哈。
  • 此时我们点击预览,大小变成了1718KB

  • 从此想要预览就很方便了呀

二、当一个taro的工程,使用taro UI 组件时,Current值错误

  • 官方issues
  • 主要是因为组件库引用的 @tarojs/runtime 会是 UI 库文件夹里 node_modules 的一份
  • 而项目使用的 @tarojs/runtime 是项目顶层 node_modules 的那份。两个包不一致导致 UI 库获取闭包的 Current 值错误。
  • 使用 webpack alias 强行指定使用顶层 node_modules 的 @tarojs/runtime即可解决此问题

具体如:

const path = require("path");
const Chain = require("webpack-chain");
exports.default = (appPath) => {
    const chain = new Chain();
    chain.merge({
        resolve: {
            alias: {
                // 小程序使用 regenerator-runtime@0.11
                'regenerator-runtime': require.resolve('regenerator-runtime'),
                // 开发组件库时 link 到本地调试,runtime 包需要指向本地 node_modules 顶层的 runtime,保证闭包值 Current 一致
                '@tarojs/runtime': require.resolve('@tarojs/runtime')
            }
        },
    });
    return chain;
};
  • 版本升级之后,在使用库及ui库都可以获取到 Current
  • 使用库如

  • ui库如

升级之后就是正常的使用,就都可以获取到啦。
查看原文

赞 0 收藏 0 评论 0

写代码的小可爱 发布了文章 · 9月5日

taro项目rollup打包UI库实战

【前置介绍】

  • tarob版本3.0.7
  • 开发独立的组件【非单纯的UI组件】,打包集成发布到npm上,供各业务使用。
  • 项目目录结构就使用taro脚手架init出来即可。如图

打包配置

  • 按照taro官方给出的打包方式https://github.com/NervJS/taro-uilib-react
  • 我没有按照官方的项目架构开发组件,只是将rollup配置添加到了init出来的项目中。我的配置如下:

1、首先将配置文件放进来,如下目录

  • 每个文件内容如:
  • tsconfig.build.json
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "jsx": "react",
    "jsxFactory": "React.createElement",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveConstEnums": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strictNullChecks": true,
    "resolveJsonModule": true,
    "target": "es2017",
    "module": "es6",
    "outDir": "./lib",
    "allowJs": true,
    "checkJs": false
  },
  "compileOnSave": false,
  "exclude": [
    "node_modules/*",
    "dist/*"
  ],
  "include": [
    "src/**/*"
  ]
}
  • tsconfig.json
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "jsx": "react",
    "jsxFactory": "React.createElement",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveConstEnums": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strictNullChecks": true,
    "resolveJsonModule": true,
    "target": "es2017",
    "module": "commonjs",
    "baseUrl": "./",
    "typeRoots": [
      "node_modules/@types",
      "types"
    ]
  },
  "compileOnSave": false,
  "exclude": [
    "node_modules/*",
    "dist/*"
  ]
}
  • tsconfig.rollup.json
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "jsx": "react",
    "jsxFactory": "React.createElement",
    "moduleResolution": "node",
    "noImplicitAny": false,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveConstEnums": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strictNullChecks": true,
    "resolveJsonModule": true,
    "target": "es5",
    "module": "es6",
    "downlevelIteration": true,
    "baseUrl": ".",
    "types": ["node"],
    "allowJs": true,
    "checkJs": false
  },
  "compileOnSave": false,
  "exclude": [
    "node_modules/*",
    "dist/*"
  ],
  "include": [
    "src/**/*"
  ]
}
  • 配置package.json文件如

  • 相关代码如下,注意截图的位置:
{
  "browser": "dist/index.umd.js",
  "module": "dist/index.esm.js",
  "main": "dist/index.js",
  "main:h5": "dist/index.esm.js",
  "source": "src/index.ts",
}
  • package.json文件可以打包成功,是依赖一些插件的,具体如,如果你打包失败,根据报错排查相关依赖是否安装
{
 "devDependencies": {
    "rollup": "^2.3.1",
    "rollup-plugin-babel": "^4.4.0",
    "rollup-plugin-copy": "^3.3.0",
    "rollup-plugin-jsx": "^1.0.3",
    "rollup-plugin-node-resolve": "^5.2.0",
    "rollup-plugin-typescript2": "^0.27.1",
    "rollup-plugin-visualizer": "^4.0.2",
    "@rollup/plugin-commonjs": "^11.0.2",
    "@rollup/plugin-json": "^4.0.2",
    "@rollup/plugin-node-resolve": "^7.1.1",
    "rollup-plugin-postcss": "^3.1.6",
    "@rollup/plugin-alias": "^3.1.1",
    "@rollup/plugin-replace": "^2.3.3",
    "@babel/plugin-proposal-class-properties": "^7.10.4",
    "@babel/plugin-transform-react-jsx": "^7.10.4",
    "@babel/preset-env": "^7.10.4",
    "@babel/preset-react": "^7.10.4",
    "@babel/preset-typescript": "^7.10.4",
    "babel-plugin-external-helpers": "^6.22.0",
    "babel-preset-latest": "^6.24.1"
 }
}
 
  • 为了方便你排查问题,贴上我的全部package.json文件
{
  "name": "@xnfe/apl-taro-order",
  "version": "1.0.7",
  "private": false,
  "description": "",
  "templateInfo": {
    "name": "default",
    "typescript": false,
    "css": "less"
  },
  "browser": "dist/index.umd.js",
  "module": "dist/index.esm.js",
  "main": "dist/index.js",
  "main:h5": "dist/index.esm.js",
  "source": "src/index.ts",
  "scripts": {
    "build:weapp": "taro build --type weapp",
    "build:swan": "taro build --type swan",
    "build:alipay": "taro build --type alipay",
    "build:tt": "taro build --type tt",
    "build:h5": "taro build --type h5",
    "build:rn": "taro build --type rn",
    "build:qq": "taro build --type qq",
    "build:jd": "taro build --type jd",
    "build:quickapp": "taro build --type quickapp",
    "dev:weapp": "npm run build:weapp -- --watch",
    "dev:swan": "npm run build:swan -- --watch",
    "dev:alipay": "npm run build:alipay -- --watch",
    "dev:tt": "npm run build:tt -- --watch",
    "dev:h5": "npm run build:h5 -- --watch",
    "dev:rn": "npm run build:rn -- --watch",
    "dev:qq": "npm run build:qq -- --watch",
    "dev:jd": "npm run build:jd -- --watch",
    "dev:quickapp": "npm run build:quickapp -- --watch",
    "build": "yarn run build:rollup && yarn run build:lib",
    "build:lib": "tsc --project ./tsconfig.build.json",
    "build:rollup": "rollup --config ./config/rollup.config.js"
  },
  "browserslist": [
    "last 3 versions",
    "Android >= 4.1",
    "ios >= 8"
  ],
  "author": "",
  "dependencies": {
    "@babel/helper-create-regexp-features-plugin": "^7.10.4",
    "@babel/runtime": "^7.7.7",
    "@tarojs/components": "3.0.7",
    "@tarojs/react": "3.0.7",
    "@tarojs/runtime": "3.0.7",
    "@tarojs/taro": "3.0.7",
    "classnames": "^2.2.6",
    "clipboard": "^2.0.6",
    "less": "^3.12.2",
    "react": "^16.10.0",
    "react-dom": "^16.10.0",
    "rollup-plugin-terser": "^7.0.1"
  },
  "devDependencies": {
    "@babel/core": "^7.8.0",
    "@babel/plugin-proposal-class-properties": "^7.10.4",
    "@babel/plugin-transform-react-jsx": "^7.10.4",
    "@babel/preset-env": "^7.10.4",
    "@babel/preset-react": "^7.10.4",
    "@babel/preset-typescript": "^7.10.4",
    "@jdreact/wx-min-crypto": "^1.0.0",
    "@rollup/plugin-alias": "^3.1.1",
    "@rollup/plugin-commonjs": "^11.0.2",
    "@rollup/plugin-json": "^4.0.2",
    "@rollup/plugin-node-resolve": "^7.1.1",
    "@rollup/plugin-replace": "^2.3.3",
    "@tarojs/mini-runner": "3.0.7",
    "@tarojs/webpack-runner": "3.0.7",
    "@types/react": "^16.0.0",
    "@types/webpack-env": "^1.13.6",
    "babel-plugin-external-helpers": "^6.22.0",
    "babel-preset-latest": "^6.24.1",
    "babel-preset-taro": "^3.0.2",
    "eslint": "^6.8.0",
    "eslint-config-taro": "3.0.7",
    "eslint-plugin-import": "^2.12.0",
    "eslint-plugin-react": "^7.8.2",
    "eslint-plugin-react-hooks": "^1.6.1",
    "husky": "^4.2.3",
    "lint-staged": "7.2.2",
    "prettier": "^2.0.4",
    "rollup": "^2.3.1",
    "rollup-plugin-babel": "^4.4.0",
    "rollup-plugin-clear": "^2.0.7",
    "rollup-plugin-copy": "^3.3.0",
    "rollup-plugin-jsx": "^1.0.3",
    "rollup-plugin-node-resolve": "^5.2.0",
    "rollup-plugin-postcss": "^3.1.6",
    "rollup-plugin-terser": "^7.0.1",
    "rollup-plugin-typescript2": "^0.27.1",
    "rollup-plugin-uglify": "^6.0.4",
    "rollup-plugin-visualizer": "^4.0.2",
    "stylelint": "9.3.0"
  },
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "linters": {
      "*.{js,jsx}": [
        "prettier --write",
        "eslint",
        "git add"
      ]
    },
    "ignore": [
      "dist_h5/**/*.js",
      "dist_jd/**/*.js",
      "dist_weapp/**/*.js",
      "dist_tt/**/*.js",
      "dist_qq/**/*.js",
      "jd_weapp/**/*.js",
      "src/pages/login/loginSdk/beta/*.js",
      "src/pages/login/loginSdk/prod/*.js",
      "src/utils/taroAppReport.js",
      "lib/**/*.js",
      "dist/**/*.js",
      "public/**/*.js"
    ]
  }
}
  • 现在在我们的src下添加index.ts文件,内容包含你要导出的组件或者封装的js方法

  • 然后在我们的config目录下,新建rollup.config.js

  • rollup.config.analyze.js代码如
import RollupVisualizer from 'rollup-plugin-visualizer'
import defaultConfig from './rollup.config'

export default Object.assign({}, defaultConfig, {
  plugins: [...defaultConfig.plugins, RollupVisualizer()],
})
  • rollup.config.js代码如
import NodePath from 'path'
import RollupJson from '@rollup/plugin-json'
import RollupNodeResolve from '@rollup/plugin-node-resolve'
import RollupCommonjs from '@rollup/plugin-commonjs'
import RollupTypescript from 'rollup-plugin-typescript2'
import RollupCopy from 'rollup-plugin-copy'
import RollupBabel from 'rollup-plugin-babel'
import RollupPostCss from 'rollup-plugin-postcss'
import RollupReplace from '@rollup/plugin-replace'
import RollupClear from 'rollup-plugin-clear'
import { terser } from 'rollup-plugin-terser'
import Package from '../package.json'

const resolveFile = (path) => NodePath.resolve(__dirname, '..', path)

const externalPackages = ['react', 'react-dom', '@tarojs/components', '@tarojs/runtime', '@tarojs/taro', '@tarojs/react']

export default {
  input: resolveFile(Package.source),
  output: [
    {
      file: resolveFile(Package.main),
      format: 'cjs',
      sourcemap: true,
    },
    {
      file: resolveFile(Package.module),
      format: 'es',
      sourcemap: true,
    },
    {
      file: resolveFile(Package.browser),
      format: 'umd',
      name: '@xnfe/apl-taro-order',
      sourcemap: true,
      globals: {
        react: 'React',
        '@tarojs/components': 'components',
        '@tarojs/taro': 'Taro',
      },
    },
  ],
  external: externalPackages,
  plugins: [
    RollupClear({
      targets: ['dist'], // 每次打包清空dist目录,重新生成
      watch: true,
    }),
    RollupPostCss({
      use: [
        [
          'less',
          {
            javascriptEnabled: true,
          },
        ],
      ],
      extract: `style/index.css`,
      extensions: ['.css', '.less'],
    }),
    RollupReplace({
      public: './public', //替换字符用
      include: 'src/mta/taroMta.js', //一定要指定include 不是仅仅引用会替换,全部都会
    }),
    RollupNodeResolve({
      customResolveOptions: {
        moduleDirectory: 'node_modules',
      },
    }),
    RollupCommonjs({
      include: [/\/node_modules\//],
    }),
    RollupJson(),
    RollupBabel({
      exclude: ['node_modules/**'],
      presets: ['@babel/env', '@babel/preset-react'],
      plugins: ['@babel/plugin-proposal-class-properties'],
      runtimeHelpers: true,
    }),
    RollupTypescript({
      tsconfig: resolveFile('tsconfig.rollup.json'),
    }),
    RollupCopy({
      targets: [
        {
          src: resolveFile('src/style'),
          dest: resolveFile('dist'),
        },
        {
          src: resolveFile('public'),
          dest: resolveFile('dist'),
        },
      ],
    }),
    terser(),
  ],
}
  • 添加完如上配置之后,在我们的package.json的scripts中,新增rollup build命令,如


--代码

 "scripts": {
    "build": "yarn run build:rollup && yarn run build:lib",
    "build:lib": "tsc --project ./tsconfig.build.json",
    "build:rollup": "rollup --config ./config/rollup.config.js"
  },
  • 最后执行 npm run build即可呀。
  • 如果关于rollup.config.js中插件及相关配置不知道是什么用的,我下次做一个介绍。
  • 如果那你使用过程中有什么问题可以留言呀。
查看原文

赞 0 收藏 0 评论 0

写代码的小可爱 发布了文章 · 9月4日

移动端手机抓包

1、电脑安装抓包工具

每一次抓包前都要打开这个工具

电脑抓包依赖无线WI-FI,连接windows的共享WI-FI,如果没有windows本,找一个身边有windows电脑的人。

目前我的是共享Wi-Fi是(**)

切换MacWi-Fi为,密码:**

  1. 手机连接Mac的Wifi,并且设置代理

  1. Mac打开钥匙串,搜索charles然后县级信任中的始终信任。
  2. 然后打开charles工具-->Proxy ---> Proxy settings ---> Proxies{下的端口号设置为8888}
  3. 手机设置代理要注意其IP地址,端口号要和charles工具设置的端口号一致。
  4. 手机打开safari浏览器安装证书,安装证书之后点击找到对应手机的信任证书地方,进行证书信任。
  5. 然后电脑打开hosts,手机浏览器输入地址就能正常访问了。
  6. 详细见文章
查看原文

赞 0 收藏 0 评论 0

写代码的小可爱 发布了文章 · 9月2日

Dockerfile部署

单页 - Dockerfile构建

1、项目工程中新建文件夹docker目录

git clone https://xx.xx.com/example.git
cd example 
mkdir docker
cd docker
touch Dockerfile
touch nginx.conf
  • Dockerfile
FROM XX-new-centos6-nginx
RUN mkdir -p /app
COPY ./dist /app
COPY ./docker/nginx.conf /opt/nginx/conf/domains
WORKDIR /app
ENTRYPOINT mkdir -p /dev/shm/nginx_temp/client_body && mkdir -p /export/Logs/servers/nginx/logs && nginx -c /opt/nginx/conf/nginx.conf && sleep 9999999d
  • nginx.conf
  • history 模式
server {
  listen 80;
  server_name www.ww.com;
  location / {
    root /app;
    expires 1d;
    try_files $uri $uri/ /index.html;
  }
}
  • hash模式
server {
  listen 80;
  server_name www.ww.com;
  location / {
    root /app;
    expires 1d;
  }
}

2、使用Dockerfile构建

  • 基础镜像: XX-new-centos6-nginx [对应Dockerfile中 FORM]
  • 源码地址: https://xx.xx.com/example.git [你要部署的git地址仓库]
  • Git信息: 对应部署分支
  • Dockerfile路径: docker/Dockerfile
  • 然后点击立即构建即可构建前端docker镜像
查看原文

赞 0 收藏 0 评论 0

写代码的小可爱 发布了文章 · 9月2日

小白入门上手使用多端开发框架 Taro

开发环境搭建

1、需要安装的软件【点击对应链接下载即可】

Windows / Mac

环境依赖
  • nodejs [推荐安装LTS版本]
编码工具/编辑器
小程序端工具

2、开发环境配置

  • node 安装成功之后
  • 打开【终端】查看下node的版本
  • node -v, 出现v xx.xx.x 的相关信息,代表你安装成功了
  • 在终端执行如下步骤
# 全局安装taro的命令行工具
npm install @tarojs/cli 

# 出现👽 Taro v x.x.x 证明安装成功
taro 

3、简单 Hello World 项目体验

  • project 是你生成项目的名字,可以是你想称呼的。例如 example、test、demo等
  • 然后一路回车,首次走默认的即可
# taro init 之后,见下图1,init成功之后,见下图2
taro init project

cd project

# 安装依赖,避免init 项目过程中依赖安装不完整的情况
npm install

-[注意]:如果你npm install 出现错误,别慌,按照如下操作走一波。

rm -rf node_modules package-lock.json
npm install

图1
图2:

4、生成项目目录介绍

.

├── babel.config.js             # Babel 配置
├── .eslintrc.js                # ESLint 配置
├── config                      # 编译配置目录
│   ├── dev.js                  # 开发模式配置
│   ├── index.js                # 默认配置
│   └── prod.js                 # 生产模式配置
├── package.json                # 项目的核心。它包含名称、描述和版本之类的信息,以及运行、开发以及有选择地将项目发布到 NPM 所需的信息。
├── dist                        # 打包目录
├── project.config.json         # 小程序项目配置
├── src # 源码目录
│   ├── app.config.js           # 全局配置
│   ├── app.less                 # 全局 样式
│   ├── app.js                  # 入口组件
│   ├── index.html              # H5 入口 HTML
│   └── pages                   # 页面
│       └── index
│           ├── index.config.js # 页面配置
│           ├── index.less       # 页面 CSS
│           └── index.jsx       # 页面组件,如果是 Vue 项目,此文件为index.vue

.

5、本地开发环境启动

  • 注:npm run dev:h5/weapp/tt 等都是本地开发热更新,改代码预览实施生效.
  • 打开终端,cd 到你项目工程的目录,分别执行如下命令。
  • 使用webstorm 编辑器,可以使用另一种方式启动。如

  • 使用vscode 编辑器,点击右侧上方菜单栏目【Terminal】--【New Terminal】-- 就会在当前项目路径下方打开一个终端面板。,输入如下对应命令即可启动对应端的服务。

H5
cd your project
npm run dev:h5
  • 见如下图,证明启动服务成功,服务启动成功之后,会自动在浏览器打开该listening网址 http://0.0.0.0:10089

  • 效果图如:
weapp 微信小程序
npm run dev:weapp
  • 执行完如上命令之后,会把代码转换成小程序端可运行代码在dist目录下

见下图,证明服务启动成功

  • 生成的文件目录如:
  • 打开下载好的微信开发工具
  • 点击小程序--》导入项目---》目录【打开项目所在路径,到dist/即可】---》AppID有就填写你自己真是的值,没有就是默认 --》最后点击导入就会打开。如

  • 效果图如:
tt 头条小程序/抖音小程序
npm run dev:tt
  • 执行完如上命令之后,会把代码转换成小程序端可运行代码在dist目录下

见下图,证明服务启动成功

  • 生成的文件目录如
    • 打开下载好的字节跳动开发工具
  • 点击小程序--》导入---》【打开项目所在目录,到dist/即可】如

  • 效果图如:
百度 百度小程序
npm run dev:swan
  • 执行完如上命令之后,会把代码转换成小程序端可运行代码在dist目录下

见下图,证明服务启动成功

  • 生成的文件目录如
    • 打开下载好的百度开发工具
  • 点击小程序--》加号 ➕ ---》导入项目---》【打开项目所在目录,到dist/即可】--》无AppID,点击测试号


  • 效果图如:
注意如果你不知道对应端的小程序应该启动什么命令,如下:
npm run dev:tt # 头条
npm run dev:weapp # 微信
npm run dev:swan # 百度
npm run dev:alipy # 支付宝
npm run dev:qq # QQ
npm run dev:jd # 京东
npm run dev:h5 # H5
npm run dev:quickapp # 快应用

初识、写 Taro

一、简单的文本渲染

import React, { Component } from 'react'
import { View, Text } from '@tarojs/components'

export default class Index extends Component {
  constructor() {
    super();
    this.state = {
      defaultValue: 'Hello World!!'
    };
  }

  componentWillMount() {}

  componentDidMount() {}

  componentWillUnmount() {}

  componentDidShow() {}

  componentDidHide() {}

  render() {
    const { defaultValue } = this.state

    return (
      <View className='home'>
        {/* 文本渲染 */}
        <Text style={{ color: 'red' }}>{defaultValue}</Text>
      </View>
    );
  }
}

二、JSX 数组只能使用 Array.map

import React, { Component } from 'react'
import { View, Text } from '@tarojs/components'

const testData = [
  {
    label: '星期一',
    id: 1,
    value: 'Monday',
  },
  {
    label: '星期二',
    id: 2,
    value: 'Tuesday',
  },
  {
    label: '星期三',
    id: 3,
    value: 'Wednesday',
  },
];

export default class Index extends Component {
  constructor() {
    super();
  }

  componentWillMount() {}

  componentDidMount() {}

  componentWillUnmount() {}

  componentDidShow() {}

  componentDidHide() {}

  render() {
    return (
      <View className='home'>
        {testData.map((item) => {
          return (
            <View key={item.id} className='home__week'>
              <Text>{item.label}</Text>
              <Text>{item.value}</Text>
            </View>
          );
        })}
      </View>
    );
  }
}

三、绑定点击事件

import React, { Component } from 'react'
import { View, Text } from '@tarojs/components'



export default class Index extends Component {
  constructor() {
    super();
     this.state = {
      defaultValue: 'Hello World!!',
      number: 0,
    }
  }

  componentWillMount() {}

  componentDidMount() {}

  componentWillUnmount() {}

  componentDidShow() {}

  componentDidHide() {}

  handleClick1 = () => {
    this.setState({
      defaultValue: "1、2、3、变了!!!",
    });
    console.log("你现在点击我了");
  };
  
  render() {
      const { defaultValue, number } = this.state;
    return (
      <View className='home'>
           {/* 文本渲染 */}
        <Text style={{ color: "red" }}>{defaultValue}</Text>
        
        {/* 绑定点击事件,写法1 */}
        <View className='home__btn'>
          <Text onClick={this.handleClick1} className='home__btn--txt'>
            点击我改变defaultValue
          </Text>
        </View>

        {/* 绑定点击事件,写法2,不推荐,根据实际场景,例如error重新请求数据 onClick={()=>this.fetchData()} */}
        <View
          onClick={() => {
              // 操作state自身最好使用如下方式
            this.setState((prevState) => {
              return {
                number: prevState.number + 1,
              };
            });
          }}
          style={styles.viewStyle}
        >
          <Text style={styles.viewStyleFont}>{number}</Text>
        </View>
      </View>
    );
  }
}

const styles = {
  viewStyle: {
    background: 'pink',
    color: '#fff'
  },
  viewStyleFont: {
    fontSize: '14px'
  }
}
.home {
  padding: 16Px;

  &__week {
    display: flex;
    justify-content: space-between;
    font-size: 16Px;
  }

  &__btn {
    width: 163Px;
    height: 38Px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, rgba(242,20,12,1) 0%,rgba(242,39,12,1) 70%,rgba(242,77,12,1) 100%);
    border-radius: 26Px;
    &--txt {
        font-size: 14Px;
        color: rgba(255,255,255,1);
    }
  }
}
  • state
  • 上述操作修改了state,简单说下,不要直接修改state,例如,this.state.defaultValue = '1、2、3',因为此种方式不会重新渲染组件,不走render,如果安装了elsint校验,会过不去的。
  • State的更新可能是异步的,立马执行之后,是无法直接获取到最新的 state ,React 其实会维护着一个 state 的更新队列,每次调用 setState 都会先把当前修改的 state 推进这个队列,在最后,React 会对这个队列进行合并处理,然后去执行回调。根据最终的合并结果再去走下面的流程(更新虚拟dom,触发渲染真实dom)
  • state使用的三种方式
// 第一种
this.setState({ number: this.state.number + 1 }) // this.state.number 0  
// 第二种
this.setState({ number: this.state.number + 1 },() => {
    console.log("callback: ", this.state.number); // 2
 });
// 第三种
this.setState(prevState => {
  console.log("func: " + prevState.number); // 1
      return {
        number: prevState.number + 1
      };
    },()=>{
      console.log('now '+ this.state.number) // 2
 });

四、父子组件通信

  • props 传值通信
  • 子组件

import React, { Component } from 'react';
import { View, Text } from '@tarojs/components';

export default class Child extends Component {
  static defaultProps = {
    langName: '',
    enName: '',
    onNameEvent: () => {},
  };

  constructor() {
    super();
  }

  componentWillMount() {}

  componentDidMount() {}

  componentWillUnmount() {}

  componentDidShow() {}

  componentDidHide() {}

  emitNameEvent = (langName) => {
    this.props.onNameEvent(langName);
  };

  // 界面描述
  render() {
    const { langName, enName } = this.props;

    return (
      <View style={styles.container}>
        <Text onClick={() => this.emitNameEvent(langName)}>{langName}</Text>
        <Text>{enName}</Text>
      </View>
    );
  }
}

const styles = {
  container: {
    width: '100%',
    display: 'flex',
    justifyContent: 'space-between',
  },
};
  • 父组件
import React, { Component } from 'react'
import { View, Text } from '@tarojs/components'
import Child from './components/test';

const testData = [
  {
    label: '星期一',
    id: 1,
    value: 'Monday',
  },
  {
    label: '星期二',
    id: 2,
    value: 'Tuesday',
  },
  {
    label: '星期三',
    id: 3,
    value: 'Wednesday',
  },
];

export default class Index extends Component {
  constructor() {
    super();
  }

  componentWillMount() {}

  componentDidMount() {}

  componentWillUnmount() {}

  componentDidShow() {}

  componentDidHide() {}

  render() {
    return (
      <View className='home'>
        {testData.map((item) => {
          return (
            <View key={item.id} className='home__week'>
              <Child
                langName={item.label}
                enName={item.value}
                onNameEvent={(name) => {
                  console.log('拿到子组件的name',name)
                }}
              />
            </View>
          );
        })}
      </View>
    );
  }
}

四、生命周期介绍

  • 组件的生命周期,指的是一个 React 组件从挂载,更新,销毁过程中会执行的生命钩子函数。
class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  componentWillMount() {}
 
  componentDidMount() {}

  componentWillUpdate(nextProps, nextState) {}
    
  componentWillReceiveProps(nextProps) {}  
  
  componentDidUpdate(prevProps, prevState) {}

  shouldComponentUpdate(nextProps, nextState) {}

  componentWillUnmount() {}
  
  render() {
    return (
    <View>
        <Text>Hello, world!</Text>
        <Text>现在的时间是 {this.state.date.toLocaleTimeString()}.</Text>
      </View>
    );
  }
}

上述组件中生命周期一一简单叙述

初始化阶段
  • constructor,顾名思义,组件的构造函数。一般会在这里进行 state 的初始化,事件的绑定等
挂载阶段
  • componentWillMount, 组件挂载到dom之前调用。是当组件在进行挂载操作前,执行的函数,一般紧跟着 constructor 函数后执行
  • componentDidMount,是当组件挂载在 dom 节点后执行。全程只执行一次,一般会在这里执行一些异步数据的拉取等动作
更新阶段
  • shouldComponentUpdate,比较当前组件的this.props/this.state和传入的nextProps/nextState,是否需要重渲染的判断,未改变返回false,当前组件更新停止。若组件的state与props发生改变则返回true,当前组件的更新开始。可以减少组件不必要的渲染,优化组件性能。渲染优化。
  • componentWillReceiveProps,传入的参数(nextProps)为父组件传递给子组件的新props,通过本作用域中的this.state与传过来的nextProps进行比较来得出是否要重新调用render进行渲染,可以有效减少无效渲染次数,并提高周期的开发效率。你可以在这里根据新的 props 来执行一些相关的操作,例如某些功能初始化等
  • componentWillUpdate,当组件在进行render更新之前的预渲染函数。[基本不用]
  • componentDidUpdate,当组件完成更新时,会执行的函数,传入两个参数是 prevProps 、prevState,为组件更新前的props和state,需要重点注意的是,更新阶段内不许使用setState方法进行state值的修改,否则会引起render的循环重渲染。[基本不用]
卸载阶段
  • componentWillUnmount,当组件准备销毁时执行。在这里一般可以执行一些回收的工作,例如 - clearInterval(this.timer) 这种对定时器的回收操作,清除componentDidMount中手动创建的DOM元素等,以避免引起内存泄漏。

五、绑定样式

  • 方式一:行内
{/* 例如文本渲染,设置文字颜色 */}
<Text style={{ color: 'red' }}>{defaultValue}</Text>
  • 方式二、行内绑定变量
  {/* 绑定点击事件,写法2 */}
  <View style={styles.viewStyle}>
    <Text style={styles.viewStyleFont}>{number}</Text>
  </View>
        
 const styles = {
  viewStyle: {
    background: 'pink',
    color: '#fff',
  },
  viewStyleFont: {
    fontSize: '14px',
  },
};
  • 方式三、绑定class样式类名
  • 样式命名,遵循BEM规范
 {/* 手写按钮 */}
 <View className='home__btn'>
   <Text onClick={this.handleClick1} className='home__btn--txt'>
     点击我改变Hello World
    </Text>
  </View>
.home {
  padding: 16Px;
  &__btn {
    width: 163Px;
    height: 38Px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(135deg, rgba(242,20,12,1) 0%,rgba(242,39,12,1) 70%,rgba(242,77,12,1) 100%);
    border-radius: 26Px;
    &--txt {
        font-size: 14Px;
        color: rgba(255,255,255,1);
    }
  }
}

入门

  • 渐进式入门教程【这篇教程适合对喜欢边学边做或小程序开发完全没有了解的开发者】

深入学习

[React、Vue两大框架根据实际使用技术栈学习]

进阶

结语

查看原文

赞 0 收藏 0 评论 0

写代码的小可爱 发布了文章 · 8月29日

webstorm 文件识别异常问题

webstorm 使用问题

  • 你发现 .js、.vue、.jsx 文件中render方法不管用了,并且import的文件路径都使用cmd 点不进去。这是因为 .js及某个文件被识别为txt文件。
  • 解决方式如下
  • 左上角 webstorm -> Preferences -> Editor -> File Types -> Recognized File Types 下的【Text】文件。然后在下边的 File Name Patterns 找到对应的.js、.vue、.jsx文件,点击减号- 即可消除。
  • 详如图
  • image.png

image.png

查看原文

赞 0 收藏 0 评论 0

写代码的小可爱 发布了文章 · 8月19日

git submodule 子仓库使用

git仓库嵌套子仓库使用方案

  • 话不多说,直接贴代码
git clone https://father.git
cd father
# 子仓库1
git submodule add http://child1.git
git status
git commit -am "add child1"
git pull origin branch
git push origin branch
# 子仓库2
git submodule add http://child2.git
git commit -am "add child2"
git pull origin branch
git push origin branch
  • 如果git submodule 过程中遇到问题如
warning: adding embedded git repository: 子仓库name
hint: You've added another git repository inside your current repository.
hint: Clones of the outer repository will not contain the contents of
hint: the embedded repository and will not know how to obtain it.
hint: If you meant to add a submodule, use:
hint: 
hint:     git submodule add <url> 子仓库name
hint: 
hint: If you added this path by mistake, you can remove it from the
hint: index with:
hint: 
hint:     git rm --cached 子仓库name
  • 这个时候就证明你没有submodule add 成功,需要将缓存中的清除。
git rm --cached 子仓库name,此时如果报错如下:
# error
fatal: not removing 'module-name' recursively without -r
# 将命令修改成
git rm -r --cached module-name
  • submodule add 成功之后 git status 查看可看到 new file .gitmodules
  • 这个时候证明成功了。

关于子仓库的使用希望能帮到你,有问题可以留言。

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 1 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • 量子计划

    vue的项目,个人全部开发,交互以及视觉以及逻辑处理

注册于 2018-07-26
个人主页被 201 人浏览